Skip to content

Commit da8c31f

Browse files
committed
lint @inheritDoc tags for correct canonical references
1 parent 2d1e3ea commit da8c31f

File tree

5 files changed

+122
-4
lines changed

5 files changed

+122
-4
lines changed

config/apiExtractor.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawnSync } from "node:child_process";
22
import fs from "node:fs";
3-
import { join } from "node:path";
3+
import { readFile, writeFile } from "node:fs/promises";
4+
import { format, join, parse } from "node:path";
45
import { parseArgs } from "node:util";
56
import * as path from "path";
67

@@ -84,10 +85,36 @@ try {
8485
})
8586
);
8687

87-
await buildReport("@apollo/client", entryPointFile, "docModel");
88+
const result = await buildReport(
89+
"@apollo/client",
90+
entryPointFile,
91+
"docModel"
92+
);
8893
if (process.exitCode === 50) {
8994
process.exitCode = 0; // if there were only warnings, we still want to exit with 0
9095
}
96+
97+
console.log("Creating file with all possible canonical references...");
98+
const canonicalReferences = new Set<string>();
99+
const file = await readFile(result.extractorConfig.apiJsonFilePath, "utf8");
100+
JSON.parse(file, (key, value) => {
101+
if (
102+
key === "canonicalReference" &&
103+
typeof value === "string" &&
104+
value.startsWith("@apollo/client")
105+
) {
106+
canonicalReferences.add(value);
107+
}
108+
return undefined;
109+
});
110+
await writeFile(
111+
format({
112+
...parse(result.extractorConfig.apiJsonFilePath),
113+
base: "canonical-references.json",
114+
}),
115+
JSON.stringify([...canonicalReferences.values()], null, 2),
116+
"utf8"
117+
);
91118
}
92119

93120
if (parsed.values.generate?.includes("apiReport")) {
@@ -185,4 +212,5 @@ async function buildReport(
185212
);
186213
}
187214
}
215+
return extractorResult;
188216
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { TSESTree as AST } from "@typescript-eslint/types";
2+
import { ESLintUtils } from "@typescript-eslint/utils";
3+
import type { SourceCode } from "@typescript-eslint/utils/ts-eslint";
4+
5+
import references from "../docs/public/canonical-references.json" with { type: "json" };
6+
7+
const referenceSet = new Set(references);
8+
9+
export const validInheritDoc = ESLintUtils.RuleCreator.withoutDocs({
10+
create(context) {
11+
const source = context.sourceCode;
12+
let handled = new Set();
13+
return {
14+
"*"(node) {
15+
for (const comment of source.getCommentsBefore(node)) {
16+
if (handled.has(comment)) {
17+
continue;
18+
}
19+
handled.add(comment);
20+
if (comment.type === "Block") {
21+
const text = source.getText(comment);
22+
let match: RegExpMatchArray | null;
23+
if ((match = text.match(/@inheritDoc\s+([^\s}]+)/d))) {
24+
const canonicalReference = match[1];
25+
if (!referenceSet.has(canonicalReference)) {
26+
context.report({
27+
node: comment,
28+
loc: locForMatch(source, comment, match, 1),
29+
messageId: "invalidCanonicalReference",
30+
});
31+
}
32+
}
33+
if (
34+
(match = text.match(/@inheritdoc/di)) &&
35+
match[0] !== "@inheritDoc"
36+
) {
37+
const loc = locForMatch(source, comment, match, 0);
38+
context.report({
39+
node: comment,
40+
loc,
41+
messageId: "invalidSpelling",
42+
fix(fixer) {
43+
return fixer.replaceTextRange(
44+
[
45+
source.getIndexFromLoc(loc.start),
46+
source.getIndexFromLoc(loc.end),
47+
],
48+
"@inheritDoc"
49+
);
50+
},
51+
});
52+
}
53+
}
54+
}
55+
},
56+
};
57+
},
58+
meta: {
59+
messages: {
60+
invalidCanonicalReference: "Unknown canonical reference.",
61+
invalidSpelling: "Invalid spelling of @inheritDoc.",
62+
},
63+
type: "problem",
64+
schema: [],
65+
fixable: "code",
66+
},
67+
defaultOptions: [],
68+
});
69+
70+
function locForMatch(
71+
source: SourceCode,
72+
node: AST.NodeOrTokenData,
73+
match: RegExpMatchArray,
74+
index: number
75+
) {
76+
return {
77+
start: source.getLocFromIndex(
78+
source.getIndexFromLoc(node.loc.start) + match.indices[index][0]
79+
),
80+
end: source.getLocFromIndex(
81+
source.getIndexFromLoc(node.loc.start) + match.indices[index][1]
82+
),
83+
};
84+
}

eslint-local-rules/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { validInheritDoc } from "./canonical-references.ts";
12
import { rule as forbidActInDisabledActEnvironment } from "./forbid-act-in-disabled-act-environment.ts";
23
import {
34
importFromExport,
@@ -18,4 +19,5 @@ export default {
1819
"no-internal-import-official-export": noInternalImportOfficialExport,
1920
"no-duplicate-exports": noDuplicateExports,
2021
"no-relative-imports": noRelativeImports,
22+
"valid-inherit-doc": validInheritDoc,
2123
};

eslint-local-rules/tsconfig.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"noEmit": true,
77
"allowImportingTsExtensions": true,
88
"rewriteRelativeImportExtensions": true,
9-
"verbatimModuleSyntax": true
9+
"verbatimModuleSyntax": true,
10+
"resolveJsonModule": true,
1011
},
11-
"include": ["**/*.ts"]
12+
"include": [
13+
"**/*.ts"
14+
]
1215
}

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export default [
194194
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
195195

196196
"local-rules/require-using-disposable": "error",
197+
"local-rules/valid-inherit-doc": "error",
197198
},
198199
},
199200
...compat.extends("plugin:testing-library/react").map((config) => ({

0 commit comments

Comments
 (0)