[ESLint] Add rule to prevent self closing custom elements

Bug: None
Change-Id: I082b787fb56159ef9465fe8f0b5c3d5623ab9890
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2955365
Commit-Queue: Jan Scheffler <janscheffler@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
diff --git a/front_end/.eslintrc.js b/front_end/.eslintrc.js
index a0cb477..7d56e7b 100644
--- a/front_end/.eslintrc.js
+++ b/front_end/.eslintrc.js
@@ -27,6 +27,7 @@
         'rulesdir/lit_html_data_as_type': 2,
         'rulesdir/lit_no_style_interpolation': 2,
         'rulesdir/ban_literal_devtools_component_tag_names': 2,
+        'rulesdir/ban_self_closing_custom_element_tagnames': 2,
         'rulesdir/check_component_naming': 2,
         '@typescript-eslint/naming-convention': [
           'error', {
diff --git a/scripts/eslint_rules/lib/ban_self_closing_custom_element_tagnames.js b/scripts/eslint_rules/lib/ban_self_closing_custom_element_tagnames.js
new file mode 100644
index 0000000..c976c4e
--- /dev/null
+++ b/scripts/eslint_rules/lib/ban_self_closing_custom_element_tagnames.js
@@ -0,0 +1,46 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+'use strict';
+
+function isLitHtmlTemplateCall(taggedTemplateExpression) {
+  if (taggedTemplateExpression.name) {
+    // Call to html`` and we assume that html = LitHtml's html function.
+    return taggedTemplateExpression.name === 'html';
+  }
+
+  // Match calls to LitHtml.html``
+  return taggedTemplateExpression.object && taggedTemplateExpression.object.name === 'LitHtml' &&
+      taggedTemplateExpression.property.name === 'html';
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Check for self closing custom element tag names in Lit templates.',
+      category: 'Possible Errors',
+    },
+    fixable: 'code',
+    schema: []  // no options
+  },
+  create: function(context) {
+    return {
+      TaggedTemplateExpression(node) {
+        const isLitHtmlCall = isLitHtmlTemplateCall(node.tag);
+        if (!isLitHtmlCall) {
+          return;
+        }
+
+        const text = node.quasi.quasis.map(templatePart => templatePart.value.raw).join('@TEMPLATE_EXPRESSION()');
+
+        if (text.match(/<@TEMPLATE_EXPRESSION\(\)([^>]*?)\/>/)) {
+          context.report({
+            node,
+            message: 'Custom elements should not be self closing.',
+          });
+        }
+      },
+    };
+  }
+};
diff --git a/scripts/eslint_rules/tests/ban_self_closing_custom_element_tagnames_test.js b/scripts/eslint_rules/tests/ban_self_closing_custom_element_tagnames_test.js
new file mode 100644
index 0000000..0e7896e
--- /dev/null
+++ b/scripts/eslint_rules/tests/ban_self_closing_custom_element_tagnames_test.js
@@ -0,0 +1,64 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+'use strict';
+
+const rule = require('../lib/ban_self_closing_custom_element_tagnames.js');
+const ruleTester = new (require('eslint').RuleTester)({
+  parserOptions: {ecmaVersion: 9, sourceType: 'module'},
+  parser: require.resolve('@typescript-eslint/parser'),
+});
+
+const EXPECTED_ERROR_MESSAGE = 'Custom elements should not be self closing.';
+
+ruleTester.run('ban_self_closing_custom_element_tagnames', rule, {
+  valid: [
+    {
+      code: 'LitHtml.html`<p></p>`',
+      filename: 'front_end/components/test.ts',
+    },
+    {
+      code: 'LitHtml.html`<input />`',
+      filename: 'front_end/components/test.ts',
+    },
+    {
+      code: 'LitHtml.html`<${DataGrid.litTagName}></${DataGrid.litTagName}>`',
+      filename: 'front_end/components/test.ts',
+    },
+    {
+      code: 'LitHtml.html`<p><${DataGrid.litTagName}></${DataGrid.litTagName}></p>`',
+      filename: 'front_end/components/test.ts',
+    },
+    {
+      code:
+          'LitHtml.html`<${DataGrid1.litTagName}><${DataGrid2.litTagName}></${DataGrid2.litTagName}></${DataGrid1.litTagName}>`',
+      filename: 'front_end/components/test.ts',
+    },
+    {
+      code: 'LitHtml.html`<${DataGrid1.litTagName}>\n</${DataGrid1.litTagName}>`',
+      filename: 'front_end/components/test.ts',
+    },
+  ],
+  invalid: [
+    {
+      code: 'LitHtml.html`<${DataGrid.litTagName} />`',
+      filename: 'front_end/components/test.ts',
+      errors: [{message: EXPECTED_ERROR_MESSAGE}]
+    },
+    {
+      code: 'LitHtml.html`<p><${DataGrid.litTagName} /></p>`',
+      filename: 'front_end/components/test.ts',
+      errors: [{message: EXPECTED_ERROR_MESSAGE}]
+    },
+    {
+      code: 'LitHtml.html`<${DataGrid1.litTagName}><${DataGrid2.litTagName} /></${DataGrid1.litTagName}>`',
+      filename: 'front_end/components/test.ts',
+      errors: [{message: EXPECTED_ERROR_MESSAGE}]
+    },
+    {
+      code: 'LitHtml.html`<${DataGrid.litTagName} .data=${{test: "Hello World"}}/>`',
+      filename: 'front_end/components/test.ts',
+      errors: [{message: EXPECTED_ERROR_MESSAGE}]
+    },
+  ]
+});