Skip to content

Commit 18c7d77

Browse files
authored
feat: create no-duplicate-definitions (#360)
* feat: create `no-duplicate-definitions` * wip: complete rule definition * wip: update `README.md` * wip: add more testcases * wip: rename `ignores` to `ignore` * wip: add support for `FootnoteDefinition` * wip: add more testcases * wip: update docs * wip: docs * wip: refactor code * wip: address reviews * wip: address reviews by snitin315 * wip: address reviews by fasttime * wip: fix typos * wip: refactor logics * wip: use Set
1 parent 4ecc9dc commit 18c7d77

4 files changed

Lines changed: 497 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export default defineConfig([
7575
| :- | :- | :-: |
7676
| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Require languages for fenced code blocks | yes |
7777
| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one | yes |
78+
| [`no-duplicate-definitions`](./docs/rules/no-duplicate-definitions.md) | Disallow duplicate definitions | yes |
7879
| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document | no |
7980
| [`no-empty-definitions`](./docs/rules/no-empty-definitions.md) | Disallow empty definitions | yes |
8081
| [`no-empty-images`](./docs/rules/no-empty-images.md) | Disallow empty images | yes |
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# no-duplicate-definitions
2+
3+
Disallow duplicate definitions.
4+
5+
## Background
6+
7+
In Markdown, it's possible to define the same definition identifier multiple times. However, this is usually a mistake, as it can lead to unintended or incorrect link, image, and footnote references.
8+
9+
Please note that this rule does not report definition-style comments. For example:
10+
11+
```markdown
12+
[//]: # (This is a comment 1)
13+
[//]: <> (This is a comment 2)
14+
```
15+
16+
## Rule Details
17+
18+
> [!IMPORTANT] <!-- eslint-disable-line -- This should be fixed in https://github.com/eslint/markdown/issues/294 -->
19+
>
20+
> The `FootnoteDefinition` node is detected only when using `language` mode [`markdown/gfm`](/README.md#languages).
21+
22+
This rule warns when `Definition` and `FootnoteDefinition` type identifiers are defined multiple times. Please note that this rule is **case-insensitive**, meaning `earth` and `Earth` are treated as the same identifier.
23+
24+
Examples of **incorrect** code:
25+
26+
```markdown
27+
<!-- eslint markdown/no-duplicate-definitions: "error" -->
28+
<!-- definition -->
29+
30+
[mercury]: https://example.com/mercury/
31+
[mercury]: https://example.com/venus/
32+
33+
[earth]: https://example.com/earth/
34+
[Earth]: https://example.com/mars/
35+
36+
<!-- footnote definition -->
37+
38+
[^mercury]: Hello, Mercury!
39+
[^mercury]: Hello, Venus!
40+
41+
[^earth]: Hello, Earth!
42+
[^Earth]: Hello, Mars!
43+
```
44+
45+
Examples of **correct** code:
46+
47+
```markdown
48+
<!-- eslint markdown/no-duplicate-definitions: "error" -->
49+
<!-- definition -->
50+
51+
[mercury]: https://example.com/mercury/
52+
[venus]: https://example.com/venus/
53+
54+
<!-- footnote definition -->
55+
56+
[^mercury]: Hello, Mercury!
57+
[^venus]: Hello, Venus!
58+
59+
<!-- definition-style comment -->
60+
61+
[//]: # (This is a comment 1)
62+
[//]: <> (This is a comment 2)
63+
```
64+
65+
## Options
66+
67+
The following options are available on this rule:
68+
69+
- `allowDefinitions: Array<string>` - when specified, duplicate definitions are allowed if they match one of the identifiers in this array. This is useful for ignoring definitions that are intentionally duplicated. (default: `["//"]`)
70+
71+
Examples of **correct** code when configured as `"no-duplicate-definitions: ["error", { allowDefinitions: ["mercury"] }]`:
72+
73+
```markdown
74+
<!-- eslint markdown/no-duplicate-definitions: ["error", { allowDefinitions: ["mercury"] }] -->
75+
[mercury]: https://example.com/mercury/
76+
[mercury]: https://example.com/venus/
77+
```
78+
79+
- `allowFootnoteDefinitions: Array<string>` - when specified, duplicate footnote definitions are allowed if they match one of the identifiers in this array. This is useful for ignoring footnote definitions that are intentionally duplicated. (default: `[]`)
80+
81+
Examples of **correct** code when configured as `"no-duplicate-definitions: ["error", { allowFootnoteDefinitions: ["mercury"] }]`:
82+
83+
```markdown
84+
<!-- eslint markdown/no-duplicate-definitions: ["error", { allowFootnoteDefinitions: ["mercury"] }] -->
85+
[^mercury]: Hello, Mercury!
86+
[^mercury]: Hello, Venus!
87+
```
88+
89+
## When Not to Use It
90+
91+
If you are using a different style of definition comments, or not concerned with duplicate definitions, you can safely disable this rule.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @fileoverview Rule to prevent duplicate definitions in Markdown.
3+
* @author 루밀LuMir(lumirlumir)
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ allowDefinitions: string[], allowFootnoteDefinitions: string[]; }]; }>}
12+
* NoDuplicateDefinitionsRuleDefinition
13+
*/
14+
15+
//-----------------------------------------------------------------------------
16+
// Rule Definition
17+
//-----------------------------------------------------------------------------
18+
19+
/** @type {NoDuplicateDefinitionsRuleDefinition} */
20+
export default {
21+
meta: {
22+
type: "problem",
23+
24+
docs: {
25+
recommended: true,
26+
description: "Disallow duplicate definitions",
27+
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-duplicate-definitions.md",
28+
},
29+
30+
messages: {
31+
duplicateDefinition: "Unexpected duplicate definition found.",
32+
duplicateFootnoteDefinition:
33+
"Unexpected duplicate footnote definition found.",
34+
},
35+
36+
schema: [
37+
{
38+
type: "object",
39+
properties: {
40+
allowDefinitions: {
41+
type: "array",
42+
items: {
43+
type: "string",
44+
},
45+
uniqueItems: true,
46+
},
47+
allowFootnoteDefinitions: {
48+
type: "array",
49+
items: {
50+
type: "string",
51+
},
52+
uniqueItems: true,
53+
},
54+
},
55+
additionalProperties: false,
56+
},
57+
],
58+
59+
defaultOptions: [
60+
{
61+
allowDefinitions: ["//"],
62+
allowFootnoteDefinitions: [],
63+
},
64+
],
65+
},
66+
67+
create(context) {
68+
const allowDefinitions = new Set(context.options[0]?.allowDefinitions);
69+
const allowFootnoteDefinitions = new Set(
70+
context.options[0]?.allowFootnoteDefinitions,
71+
);
72+
73+
const definitions = new Set();
74+
const footnoteDefinitions = new Set();
75+
76+
return {
77+
definition(node) {
78+
if (allowDefinitions.has(node.identifier)) {
79+
return;
80+
}
81+
82+
if (definitions.has(node.identifier)) {
83+
context.report({
84+
node,
85+
messageId: "duplicateDefinition",
86+
});
87+
} else {
88+
definitions.add(node.identifier);
89+
}
90+
},
91+
92+
footnoteDefinition(node) {
93+
if (allowFootnoteDefinitions.has(node.identifier)) {
94+
return;
95+
}
96+
97+
if (footnoteDefinitions.has(node.identifier)) {
98+
context.report({
99+
node,
100+
messageId: "duplicateFootnoteDefinition",
101+
});
102+
} else {
103+
footnoteDefinitions.add(node.identifier);
104+
}
105+
},
106+
};
107+
},
108+
};

0 commit comments

Comments
 (0)