Skip to content

Commit 5f11a3d

Browse files
authored
feat: Create type definitions for rules (#324)
* fix: make `RuleModule` language specific * patch for deprecated `RuleContext` methods * extend `MarkdownRuleVisitor` * fix `IMarkdownSourceCode` * export generic type `MarkdownRuleDefinition` * remove patch for deprecated `RuleContext` methods * update ESLint * remove `MarkdownRuleDefinition` export
1 parent 49b33bb commit 5f11a3d

13 files changed

+190
-20
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"c8": "^10.1.2",
6767
"chai": "^5.1.1",
6868
"dedent": "^1.5.3",
69-
"eslint": "^9.15.0",
69+
"eslint": "^9.23.0",
7070
"eslint-config-eslint": "^11.0.0",
7171
"eslint-plugin-eslint-plugin": "^6.3.2",
7272
"globals": "^15.1.0",

Diff for: src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import rules from "./build/rules.js";
1919
/** @typedef {import("eslint").Linter.RulesRecord} RulesRecord*/
2020
/** @typedef {import("eslint").Linter.Config} Config*/
2121
/** @typedef {import("eslint").ESLint.Plugin} Plugin */
22-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
22+
/** @typedef {import("./types.ts").MarkdownRuleDefinition} RuleModule */
23+
/** @typedef {import("./types.ts").MarkdownRuleVisitor} MarkdownRuleVisitor */
2324
/** @typedef {import("@eslint/core").Language} Language */
2425

2526
//-----------------------------------------------------------------------------

Diff for: src/language/markdown-source-code.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { findOffsets } from "../util.js";
3232
/** @typedef {import("@eslint/core").SourceRange} SourceRange */
3333
/** @typedef {import("@eslint/core").FileProblem} FileProblem */
3434
/** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
35+
/** @typedef {import("../types.ts").IMarkdownSourceCode} IMarkdownSourceCode */
3536

3637
//-----------------------------------------------------------------------------
3738
// Helpers
@@ -135,6 +136,7 @@ function extractInlineConfigCommentsFromHTML(node) {
135136

136137
/**
137138
* Markdown Source Code Object
139+
* @implements {IMarkdownSourceCode}
138140
*/
139141
export class MarkdownSourceCode extends TextSourceCodeBase {
140142
/**

Diff for: src/rules/fenced-code-language.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
// Type Definitions
88
//-----------------------------------------------------------------------------
99

10-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
10+
/**
11+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ required?: string[]; }]; }>}
12+
* FencedCodeLanguageRuleDefinition
13+
*/
1114

1215
//-----------------------------------------------------------------------------
1316
// Rule Definition
1417
//-----------------------------------------------------------------------------
1518

16-
/** @type {RuleModule} */
19+
/** @type {FencedCodeLanguageRuleDefinition} */
1720
export default {
1821
meta: {
1922
type: "problem",

Diff for: src/rules/heading-increment.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
// Type Definitions
88
//-----------------------------------------------------------------------------
99

10-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
10+
/**
11+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12+
* HeadingIncrementRuleDefinition
13+
*/
1114

1215
//-----------------------------------------------------------------------------
1316
// Rule Definition
1417
//-----------------------------------------------------------------------------
1518

16-
/** @type {RuleModule} */
19+
/** @type {HeadingIncrementRuleDefinition} */
1720
export default {
1821
meta: {
1922
type: "problem",
@@ -40,7 +43,7 @@ export default {
4043
messageId: "skippedHeading",
4144
data: {
4245
fromLevel: lastHeadingDepth.toString(),
43-
toLevel: node.depth,
46+
toLevel: node.depth.toString(),
4447
},
4548
});
4649
}

Diff for: src/rules/no-duplicate-headings.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
// Type Definitions
88
//-----------------------------------------------------------------------------
99

10-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
10+
/**
11+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12+
* NoDuplicateHeadingsRuleDefinition
13+
*/
1114

1215
//-----------------------------------------------------------------------------
1316
// Rule Definition
1417
//-----------------------------------------------------------------------------
1518

16-
/** @type {RuleModule} */
19+
/** @type {NoDuplicateHeadingsRuleDefinition} */
1720
export default {
1821
meta: {
1922
type: "problem",

Diff for: src/rules/no-empty-links.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
// Type Definitions
77
//-----------------------------------------------------------------------------
88

9-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
9+
/**
10+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
11+
* NoEmptyLinksRuleDefinition
12+
*/
1013

1114
//-----------------------------------------------------------------------------
1215
// Rule Definition
1316
//-----------------------------------------------------------------------------
1417

15-
/** @type {RuleModule} */
18+
/** @type {NoEmptyLinksRuleDefinition} */
1619
export default {
1720
meta: {
1821
type: "problem",

Diff for: src/rules/no-html.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { findOffsets } from "../util.js";
1313
// Type Definitions
1414
//-----------------------------------------------------------------------------
1515

16-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
16+
/**
17+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: [{ allowed?: string[]; }]; }>}
18+
* NoHtmlRuleDefinition
19+
*/
1720

1821
//-----------------------------------------------------------------------------
1922
// Helpers
@@ -25,7 +28,7 @@ const htmlTagPattern = /<([a-z0-9]+(?:-[a-z0-9]+)*)/giu;
2528
// Rule Definition
2629
//-----------------------------------------------------------------------------
2730

28-
/** @type {RuleModule} */
31+
/** @type {NoHtmlRuleDefinition} */
2932
export default {
3033
meta: {
3134
type: "problem",

Diff for: src/rules/no-invalid-label-refs.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import { findOffsets, illegalShorthandTailPattern } from "../util.js";
1515

1616
/** @typedef {import("unist").Position} Position */
1717
/** @typedef {import("mdast").Text} TextNode */
18-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
18+
/**
19+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
20+
* NoInvalidLabelRuleDefinition
21+
*/
1922

2023
//-----------------------------------------------------------------------------
2124
// Helpers
@@ -120,7 +123,7 @@ function findInvalidLabelReferences(node, docText) {
120123
// Rule Definition
121124
//-----------------------------------------------------------------------------
122125

123-
/** @type {RuleModule} */
126+
/** @type {NoInvalidLabelRuleDefinition} */
124127
export default {
125128
meta: {
126129
type: "problem",

Diff for: src/rules/no-missing-label-refs.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import { findOffsets, illegalShorthandTailPattern } from "../util.js";
1515

1616
/** @typedef {import("unist").Position} Position */
1717
/** @typedef {import("mdast").Text} TextNode */
18-
/** @typedef {import("eslint").Rule.RuleModule} RuleModule */
18+
/**
19+
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
20+
* NoMissingLabelRuleDefinition
21+
*/
1922

2023
//-----------------------------------------------------------------------------
2124
// Helpers
@@ -101,7 +104,7 @@ function findMissingReferences(node, nodeText) {
101104
// Rule Definition
102105
//-----------------------------------------------------------------------------
103106

104-
/** @type {RuleModule} */
107+
/** @type {NoMissingLabelRuleDefinition} */
105108
export default {
106109
meta: {
107110
type: "problem",

Diff for: src/types.ts

+95-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
1-
import type { Node } from "mdast";
1+
//------------------------------------------------------------------------------
2+
// Imports
3+
//------------------------------------------------------------------------------
4+
5+
import type {
6+
Code,
7+
Heading,
8+
Html,
9+
Link,
10+
Node,
11+
Parent,
12+
Root,
13+
Text,
14+
} from "mdast";
215
import type { Linter } from "eslint";
16+
import type {
17+
RuleDefinition,
18+
RuleVisitor,
19+
SourceLocation,
20+
TextSourceCode,
21+
} from "@eslint/core";
22+
23+
//------------------------------------------------------------------------------
24+
// Helpers
25+
//------------------------------------------------------------------------------
26+
27+
/** Adds matching `:exit` selectors for all properties of a `RuleVisitor`. */
28+
type WithExit<RuleVisitorType extends RuleVisitor> = {
29+
[Key in keyof RuleVisitorType as
30+
| Key
31+
| `${Key & string}:exit`]: RuleVisitorType[Key];
32+
};
33+
34+
//------------------------------------------------------------------------------
35+
// Exports
36+
//------------------------------------------------------------------------------
337

438
export interface RangeMap {
539
indent: number;
@@ -20,3 +54,63 @@ export interface Block extends Node, BlockBase {
2054
export type Message = Linter.LintMessage;
2155

2256
export type RuleType = "problem" | "suggestion" | "layout";
57+
58+
/**
59+
* The `SourceCode` interface for Markdown files.
60+
*/
61+
export interface IMarkdownSourceCode
62+
extends TextSourceCode<{
63+
LangOptions: {};
64+
RootNode: Root;
65+
SyntaxElementWithLoc: Node;
66+
ConfigNode: { value: string; position: SourceLocation };
67+
}> {
68+
/**
69+
* Gets the entire source text split into an array of lines.
70+
* @returns The source text as an array of lines.
71+
*/
72+
get lines(): Array<string>;
73+
74+
/**
75+
* Gets the source code for the given node.
76+
* @param node The AST node to get the text for.
77+
* @param beforeCount The number of characters before the node to retrieve.
78+
* @param afterCount The number of characters after the node to retrieve.
79+
* @returns The text representing the AST node.
80+
*/
81+
getText(node?: Node, beforeCount?: number, afterCount?: number): string;
82+
}
83+
84+
export interface MarkdownRuleVisitor
85+
extends RuleVisitor,
86+
WithExit<{
87+
root?(node: Root): void;
88+
code?(node: Code, parent?: Parent): void;
89+
heading?(node: Heading, parent?: Parent): void;
90+
html?(node: Html, parent?: Parent): void;
91+
link?(node: Link, parent?: Parent): void;
92+
text?(node: Text, parent?: Parent): void;
93+
}> {}
94+
95+
export type MarkdownRuleDefinitionTypeOptions = {
96+
RuleOptions: unknown[];
97+
MessageIds: string;
98+
ExtRuleDocs: Record<string, unknown>;
99+
};
100+
101+
export type MarkdownRuleDefinition<
102+
Options extends Partial<MarkdownRuleDefinitionTypeOptions> = {},
103+
> = RuleDefinition<
104+
// Language specific type options (non-configurable)
105+
{
106+
LangOptions: {};
107+
Code: IMarkdownSourceCode;
108+
Visitor: MarkdownRuleVisitor;
109+
Node: Node;
110+
} & Required<
111+
// Rule specific type options (custom)
112+
Options &
113+
// Rule specific type options (defaults)
114+
Omit<MarkdownRuleDefinitionTypeOptions, keyof Options>
115+
>
116+
>;

Diff for: tests/types/types.test.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import markdown from "@eslint/markdown";
1+
import markdown, {
2+
IMarkdownSourceCode,
3+
MarkdownNode,
4+
MarkdownRuleVisitor,
5+
ParentNode,
6+
RootNode,
7+
SourceLocation,
8+
TextNode,
9+
type RuleModule,
10+
} from "@eslint/markdown";
211
import { ESLint, Linter } from "eslint";
312

413
markdown satisfies ESLint.Plugin;
@@ -35,3 +44,41 @@ typeof processorPlugins satisfies {};
3544
// Check that all recommended rule names match the names of existing rules in this plugin.
3645
null as AssertAllNamesIn<RecommendedRuleName, RuleName>;
3746
}
47+
48+
(): RuleModule => ({
49+
create({ sourceCode }): MarkdownRuleVisitor {
50+
sourceCode satisfies IMarkdownSourceCode;
51+
52+
sourceCode.ast satisfies RootNode;
53+
sourceCode.lines satisfies string[];
54+
55+
return {
56+
// Root selector
57+
root(node) {
58+
node satisfies RootNode;
59+
},
60+
61+
// Known node selector, sourceCode methods used in visitor
62+
text(node) {
63+
node satisfies TextNode;
64+
sourceCode.getText(node) satisfies string;
65+
sourceCode.getLoc(node) satisfies SourceLocation;
66+
},
67+
68+
// Known node selector with parent
69+
link(node, parent) {
70+
node satisfies MarkdownNode;
71+
parent satisfies ParentNode | undefined;
72+
},
73+
74+
// Known node selector with ":exit"
75+
"html:exit"(node, parent) {
76+
node satisfies MarkdownNode;
77+
parent satisfies ParentNode | undefined;
78+
},
79+
80+
// Unknown selectors allowed
81+
"heading[depth=1]"(node: MarkdownNode, parent?: ParentNode) {},
82+
};
83+
},
84+
});

Diff for: tools/dedupe-types.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,10 @@ files.forEach(filePath => {
3939
return true;
4040
});
4141

42-
fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8");
42+
// replace references to ../types.ts with ./types.ts
43+
const text = remainingLines
44+
.join("\n")
45+
.replace(/\.\.\/types\.ts/gu, "./types.ts");
46+
47+
fs.writeFileSync(filePath, text, "utf8");
4348
});

0 commit comments

Comments
 (0)