Skip to content

feat: added new rule table-column-count #392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default defineConfig([
| [`no-missing-atx-heading-space`](./docs/rules/no-missing-atx-heading-space.md) | Disallow headings without a space after the hash characters | yes |
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references | yes |
| [`require-alt-text`](./docs/rules/require-alt-text.md) | Require alternative text for images | yes |
| [`table-column-count`](./docs/rules/table-column-count.md) | Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row | yes |
<!-- Rule Table End -->

**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.
Expand Down
65 changes: 65 additions & 0 deletions docs/rules/table-column-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# table-column-count

Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row.

## Background

In GitHub Flavored Markdown [tables](https://github.github.com/gfm/#tables-extension-), rows should maintain a consistent number of cells. While variations are sometimes tolerated, data rows having *more* cells than the header can lead to lost data or rendering issues. This rule prevents data rows from exceeding the header's column count.

## Rule Details

> [!IMPORTANT] <!-- eslint-disable-line -- This should be fixed in https://github.com/eslint/markdown/issues/294 -->
>
> This rule relies on the `table` AST node, typically available when using a GFM-compatible parser (e.g., `language: "markdown/gfm"`).
This rule is triggered if a data row in a GFM table contains more cells than the header row. It does not flag rows with fewer cells than the header.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"It does not flag rows with fewer cells than the header."

Can I ask why this rule doesn’t report rows with fewer cells than the header? Is there a specific constraint?

Copy link
Contributor Author

@SwetaTanwar SwetaTanwar May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chose to report tables with extra cells because, in Markdown, having more cells than headers leads to data loss, those extra cells are ignored when rendering the table. On the other hand, if a row has fewer cells, the table still renders correctly, and no data is lost.

Reporting rows with fewer cells felt more like a style concern rather than a functional issue, which is why I decided not to flag those.

Examples:

Valid (fewer cells, no data loss):

Input

| Header | Header | Header |
|--------|--------|--------|
| Cell   | Cell   |        |

Output

Header Header Header
Cell Cell

Invalid (extra cell, data is lost):

Input

| Head1 | Head2 |
|-------|-------|
| R1C1  | R1C2  | R1C3 |

Output

Head1 Head2
R1C1 R1C2

As shown, the actual problem arises when there are more cells than expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference: #392 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add that explanation into the docs?


Examples of **incorrect** code for this rule:

```markdown
<!-- eslint markdown/table-column-count: "error" -->

| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 | <!-- This data row has 3 cells, header has 2 -->

| A |
| - |
| 1 | 2 | <!-- This data row has 2 cells, header has 1 -->
```

Examples of **correct** code for this rule:

```markdown
<!-- eslint markdown/table-column-count: "error" -->

<!-- Standard correct table -->
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell | Cell |

<!-- Data row with fewer cells than header (VALID for this rule) -->
| Header | Header | Header |
| ------ | ------ | ------ |
| Cell | Cell | |

<!-- Table with some empty cells (VALID for this rule) -->
| Col A | Col B | Col C |
| ----- | ----- | ----- |
| 1 | | 3 |
| 4 | 5 |

<!-- Single column table -->
| Single Header |
| ------------- |
| Single Cell |
```

## When Not To Use It

If you intentionally create Markdown tables where data rows are expected to contain more cells than the header, and you have a specific (perhaps non-standard) processing or rendering pipeline that handles this scenario correctly, you might choose to disable this rule. However, adhering to this rule is recommended for typical GFM rendering and data consistency.

## Prior Art

* [MD056 - table-column-count](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md056---table-column-count)
79 changes: 79 additions & 0 deletions src/rules/table-column-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @fileoverview Rule to disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row
* @author Sweta Tanwar (@SwetaTanwar)
*/

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>} TableColumnCountRuleDefinition
*/

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {TableColumnCountRuleDefinition} */
export default {
meta: {
type: "problem",

docs: {
recommended: true,
description:
"Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/table-column-count.md",
},

messages: {
inconsistentColumnCount:
"Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), extra data starting here will be ignored.",
},
},

create(context) {
return {
table(node) {
if (node.children.length < 1) {
return;
}

const headerRow = node.children[0];
const expectedCellsLength = headerRow.children.length;

for (let i = 1; i < node.children.length; i++) {
const currentRow = node.children[i];
const actualCellsLength = currentRow.children.length;

if (actualCellsLength > expectedCellsLength) {
const firstExtraCellNode =
currentRow.children[expectedCellsLength];

context.report({
node: currentRow,
loc: {
start: {
line: firstExtraCellNode.position.start
.line,
column: firstExtraCellNode.position.start
.column,
},
end: {
line: currentRow.position.end.line,
column: currentRow.position.end.column,
},
},
messageId: "inconsistentColumnCount",
data: {
actualCells: String(actualCellsLength),
expectedCells: String(expectedCellsLength),
},
});
}
}
},
};
},
};
204 changes: 204 additions & 0 deletions tests/rules/table-column-count.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* @fileoverview Tests for table-column-count rule.
* @author Sweta Tanwar (@SwetaTanwar)
*/

//------------------------------------------------------------------------------
// Imports
//------------------------------------------------------------------------------

import rule from "../../src/rules/table-column-count.js";
import markdown from "../../src/index.js";
import { RuleTester } from "eslint";
import dedent from "dedent";

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester({
plugins: {
markdown,
},
language: "markdown/gfm",
});

ruleTester.run("table-column-count", rule, {
valid: [
dedent`
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell | Cell |
`,
dedent`
| Header | Header | Header |
| ------ | ------ | ------ |
| Cell | Cell |
| Cell | |
`,
dedent`
| A | B |
|---|---|
| | |
| C | |
`,
`Just some text. | not a table |`,
dedent`
| Header | Header |
| ------ | ------ | ----- |
| Cell | Cell |
`,
dedent`
| Header | Header |
| ------ | ------ |
`,
dedent`
Some text before.

| H1 | H2 |
|----|----|
| D1 | D2 |

Some text after.
`,
dedent`
| Valid | Table |
| ----- | ----- |
| Row | Here |
`,
dedent`
| abc | defghi |
:-: | -----------:
bar | baz
`,
dedent`
| f|oo |
| ------ |
| b \`|\` az |
| b **|** im |
`,
dedent`
| abc | def |
| --- | --- |
| bar | baz |
> bar
`,
dedent`
| abc | def |
| --- | --- |
`,
],

invalid: [
{
code: dedent`
| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 3,
column: 17,
endLine: 3,
endColumn: 26,
},
],
},
{
code: dedent`
| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 | R3C4 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "4", expectedCells: "2" },
line: 3,
column: 17,
endLine: 3,
endColumn: 33,
},
],
},
{
code: dedent`
| A |
| - |
| 1 | 2 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "2", expectedCells: "1" },
line: 3,
column: 5,
endLine: 3,
endColumn: 10,
},
],
},
{
code: dedent`
Some introductory text.

| Header1 | Header2 |
| ------- | ------- |
| Data1 | Data2 | Data3 |
| D4 | D5 |

Some concluding text.
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 5,
column: 21,
endLine: 5,
endColumn: 30,
},
],
},
{
code: dedent`
| abc | defghi |
:-: | -----------:
bar | baz
bar | baz | bad
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 4,
column: 11,
endLine: 4,
endColumn: 16,
},
],
},
{
code: dedent`
| abc | def |
| --- | --- |
| bar | baz | Extra |
> This is a blockquote after
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 3,
column: 13,
endLine: 3,
endColumn: 22,
},
],
},
],
});