Repository: rwjblue/ember-template-lint (js)

Issue:
Require tables to be properly formed HTML.
Require `<thead>` and/or `<tbody>`.  This avoids a very nuanced (and possibly deprecated in the future) feature of glimmer that auto-inserts these tags.

Bad:

```hbs
<table>
  <tr>
    <td></td>
  </tr>
</table>
```

```hbs
<table>
  {{#each foo as |item|}}
    {{some-thing-that-has-tagName-tr item=item}}
  {{/each}}
</table>
```

```hbs
<table>
  {{some-thing item=item}}
</table>
```

Good:

```hbs
<table>
  <tbody>
    <tr>
      <td></td>
    </tr>
  </tbody>
</table>
```

```hbs
<table>
  <tbody>
    {{#each foo as |item|}}
      {{some-thing-that-has-tagName-tr item=item}}
    {{/each}}
  </tbody>
</table>
```

```hbs
<table>
  <tbody>
    {{some-thing-that-has-tagName-tr}}
  </tbody>
</table>
```


Pull request description:
Added table-groups rule
Closes #280 

I assume, that the same applies for `tfoot`.

Patch:
diff --git a/docs/rule/table-goups.md b/docs/rule/table-goups.md
new file mode 100644
index 0000000..d03f6f8
--- /dev/null
+++ b/docs/rule/table-goups.md
@@ -0,0 +1,48 @@
+## table-groups
+
+A table row should always be groouped into either a `thead`, `tbody` or `tfoot` to avoid a very nuanced (and possibly deprecated in the future) feature of glimmer that auto-inserts these tags.
+
+The rule forbids the following:
+
+```hbs
+<table>
+  <tr>
+    <td></td>
+  </tr>
+</table>
+```
+
+```hbs
+<table>
+  {{some-thing content=content}}
+</table>
+```
+
+Instead, you should write your table as:
+
+```hbs
+<table>
+  <tbody>
+    <tr>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+```
+
+```hbs
+<table>
+  <tbody>
+    {{some-thing content=content}}
+  </tbody>
+</table>
+```
+
+If you have a component which `tagName` is either `thead`, `tbody` or `tfoot`, you should disable the rule inline:
+
+```hbs
+<table>
+  {{! template-lint-disable table-groups }}
+  {{some-thing-with-tagName-tbody}}
+</table>
+```
diff --git a/docs/rules.md b/docs/rules.md
index 00c4b56..799ada9 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -16,6 +16,7 @@
 * [self-closing-void-elements](rule/self-closing-void-elements.md)
 * [simple-unless](rule/simple-unless.md)
 * [style-concatenation](rule/style-concatenation.md)
+* [table-groups](rule/table-groups.md)
 * [template-length](rule/template-length.md)
 * [triple-curlies](rule/triple-curlies.md)
 * [unused-block-params](rule/unused-block-params.md)
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 6ef9db6..060e956 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -22,5 +22,6 @@ module.exports = {
   'no-duplicate-attributes': require('./lint-duplicate-attributes'),
   'linebreak-style': require('./lint-linebreak-style'),
   'attribute-indentation': require('./lint-attribute-indentation'),
-  'template-length': require('./lint-template-length')
+  'template-length': require('./lint-template-length'),
+  'table-groups': require('./lint-table-groups')
 };
diff --git a/lib/rules/lint-table-groups.js b/lib/rules/lint-table-groups.js
new file mode 100644
index 0000000..db0f675
--- /dev/null
+++ b/lib/rules/lint-table-groups.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const Rule = require('./base');
+const message = 'Tables must have a table group (thead, tbody or tfoot).';
+
+const TABLE_GROUPS = ['thead', 'tbody', 'tfoot'];
+
+function hasTableGroup(tableNode) {
+  const tableGroup = tableNode.children.find((child) => {
+    return child.type === 'ElementNode' && TABLE_GROUPS.indexOf(child.tag) > -1;
+  });
+
+  return !!tableGroup;
+}
+
+function hasTableRows(tableNode) {
+  const tableRow = tableNode.children.find((child) => {
+    return child.type === 'ElementNode' && child.tag === 'tr';
+  });
+
+  return !!tableRow;
+}
+
+function hasChildTags(tableNode) {
+  const contentNodes = tableNode.children.filter((child) => {
+    return child.type !== 'TextNode';
+  });
+
+  return contentNodes.length > 0;
+}
+
+module.exports = class TableGroups extends Rule {
+  visitor() {
+    return {
+      ElementNode(node) {
+        if (node.tag === 'table') {
+          if (!hasChildTags(node)) {
+            return;
+          }
+
+          if (!hasTableGroup(node) || hasTableRows(node)) {
+            this.log({
+              message: message,
+              line: node.loc && node.loc.start.line,
+              column: node.loc && node.loc.start.column,
+              source: this.sourceForNode(node)
+            });
+          }
+        }
+      }
+    };
+  }
+};
+
+module.exports.message = message;


Test patch:
diff --git a/test/unit/rules/lint-table-groups-test.js b/test/unit/rules/lint-table-groups-test.js
new file mode 100644
index 0000000..92ae2ee
--- /dev/null
+++ b/test/unit/rules/lint-table-groups-test.js
@@ -0,0 +1,93 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const message = require('../../../lib/rules/lint-table-groups').message;
+
+generateRuleTests({
+  name: 'table-groups',
+
+  config: true,
+
+  good: [
+    '<table> </table>',
+    '<table><thead><tr><td>Header</td></tr></thead></table>',
+    '<table><tbody><tr><td>Body</td></tr></tbody></table>',
+    '<table><tfoot><tr><td>Footer</td></tr></tfoot></table>',
+    '<table>' +
+      '<thead>' +
+        '<tr><td>Header</td></tr>' +
+      '</thead>' +
+      '<tbody>' +
+        '<tr><td>Body</td></tr>' +
+      '</tbody>' +
+      '<tfoot>' +
+        '<tr><td>Footer</td></tr>' +
+      '</tfoot>' +
+    '</table>',
+    '<table>' +
+      '<colgroup>' +
+        '<col style="background-color: red">' +
+      '</colgroup>' +
+      '<tbody>' +
+        '<tr><td>Body</td></tr>' +
+      '</tbody>' +
+    '</table>',
+  ],
+
+  bad: [
+    {
+      template: '<table><tr><td>Foo</td></tr></table>',
+
+      result: {
+        rule: 'table-groups',
+        message: message,
+        moduleId: 'layout.hbs',
+        source: '<table><tr><td>Foo</td></tr></table>',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '<table>{{some-component}}</table>',
+
+      result: {
+        rule: 'table-groups',
+        message: message,
+        moduleId: 'layout.hbs',
+        source: '<table>{{some-component}}</table>',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '<table>{{#each foo as |bar|}}{{bar}}{{/each}}</table>',
+
+      result: {
+        rule: 'table-groups',
+        message: message,
+        moduleId: 'layout.hbs',
+        source: '<table>{{#each foo as |bar|}}{{bar}}{{/each}}</table>',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '<table>' +
+          '<tr></tr>' +
+          '<tbody><tr><td>Foo</td></tr></tbody>' +
+        '</table>',
+
+      result: {
+        rule: 'table-groups',
+        message: message,
+        moduleId: 'layout.hbs',
+        source: '<table>' +
+            '<tr></tr>' +
+            '<tbody><tr><td>Foo</td></tr></tbody>' +
+          '</table>',
+        line: 1,
+        column: 0
+      }
+    }
+  ]
+});