pasika 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/cli/index.js +32 -38
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +11 -0
- package/dist/enforcement/coverage.js +25 -29
- package/dist/enforcement/doctor.d.ts +16 -0
- package/dist/enforcement/doctor.js +249 -0
- package/dist/enforcement/types.d.ts +0 -3
- package/dist/enforcement/types.js +4 -13
- package/dist/eslint/pasika/ast-types.d.ts +122 -0
- package/dist/eslint/pasika/ast-types.js +1 -0
- package/dist/eslint/pasika/index.d.ts +58 -3
- package/dist/eslint/pasika/index.js +61 -1
- package/dist/eslint/pasika/rules/application-structure.d.ts +2 -0
- package/dist/eslint/pasika/rules/application-structure.js +152 -0
- package/dist/eslint/pasika/rules/component-conventions.d.ts +16 -0
- package/dist/eslint/pasika/rules/component-conventions.js +145 -0
- package/dist/eslint/pasika/rules/component-nesting.d.ts +12 -0
- package/dist/eslint/pasika/rules/component-nesting.js +69 -0
- package/dist/eslint/pasika/rules/config-extraction.d.ts +14 -0
- package/dist/eslint/pasika/rules/config-extraction.js +74 -0
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +2 -0
- package/dist/eslint/pasika/rules/cross-feature-import.js +75 -0
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/apply-usage.js +45 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +49 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +39 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +52 -0
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/global-css-location.js +42 -0
- package/dist/eslint/pasika/rules/css/helpers.d.ts +29 -0
- package/dist/eslint/pasika/rules/css/helpers.js +93 -0
- package/dist/eslint/pasika/rules/css/index.d.ts +13 -0
- package/dist/eslint/pasika/rules/css/index.js +25 -0
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/root-variables.js +53 -0
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/rule-tester.js +22 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +76 -0
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/surface-utility.js +41 -0
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +9 -0
- package/dist/eslint/pasika/rules/css/theme-reset.js +36 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +53 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +10 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.js +77 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +11 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +97 -0
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/data-testid-case.js +76 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +13 -1
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +46 -44
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +53 -48
- package/dist/eslint/pasika/rules/filename-case.js +28 -5
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +12 -0
- package/dist/eslint/pasika/rules/hook-complexity.js +114 -0
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +11 -0
- package/dist/eslint/pasika/rules/hook-extraction.js +55 -0
- package/dist/eslint/pasika/rules/import-through-index.d.ts +2 -0
- package/dist/eslint/pasika/rules/import-through-index.js +52 -0
- package/dist/eslint/pasika/rules/interactive-component.d.ts +10 -0
- package/dist/eslint/pasika/rules/interactive-component.js +76 -0
- package/dist/eslint/pasika/rules/json/index.d.ts +5 -0
- package/dist/eslint/pasika/rules/json/index.js +9 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +45 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +41 -0
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/json/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +9 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.js +108 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +10 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.js +65 -0
- package/dist/eslint/pasika/rules/locale-placement.d.ts +13 -0
- package/dist/eslint/pasika/rules/locale-placement.js +163 -0
- package/dist/eslint/pasika/rules/locales-location.d.ts +11 -0
- package/dist/eslint/pasika/rules/locales-location.js +56 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +29 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.js +30 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +99 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +46 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +50 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +51 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +26 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +48 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +42 -0
- package/dist/eslint/pasika/rules/md/helpers.d.ts +25 -0
- package/dist/eslint/pasika/rules/md/helpers.js +37 -0
- package/dist/eslint/pasika/rules/md/index.d.ts +27 -0
- package/dist/eslint/pasika/rules/md/index.js +53 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +36 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +39 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +22 -0
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-length.js +47 -0
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-present.js +41 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +28 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.js +34 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +58 -0
- package/dist/eslint/pasika/rules/md/project-index.d.ts +17 -0
- package/dist/eslint/pasika/rules/md/project-index.js +67 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +31 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +26 -0
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/requirement-present.js +41 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +46 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +36 -0
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/md/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.js +36 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +46 -0
- package/dist/eslint/pasika/rules/named-exports.d.ts +2 -0
- package/dist/eslint/pasika/rules/named-exports.js +49 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +10 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.js +33 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +9 -22
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +2 -0
- package/dist/eslint/pasika/rules/no-util-barrel.js +48 -0
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +9 -0
- package/dist/eslint/pasika/rules/pure-function-extract.js +92 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +13 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.js +91 -0
- package/dist/eslint/pasika/rules/stay-flat.d.ts +12 -0
- package/dist/eslint/pasika/rules/stay-flat.js +81 -0
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +2 -0
- package/dist/eslint/pasika/rules/support-folder-shape.js +59 -0
- package/dist/eslint/pasika/rules/type-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/type-extraction.js +85 -0
- package/dist/eslint/pasika/rules/ui-state.d.ts +9 -0
- package/dist/eslint/pasika/rules/ui-state.js +99 -0
- package/dist/eslint/pasika/rules/util-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/util-file-name.js +48 -0
- package/dist/eslint/pasika/rules/value-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/value-extraction.js +48 -0
- package/enforcement/registry.json +196 -190
- package/package.json +28 -19
- package/dist/enforcement/docs-check.d.ts +0 -17
- package/dist/enforcement/docs-check.js +0 -159
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getFilename, getTextContent } from "./helpers.js";
|
|
2
|
+
export const exampleHeadingDescriptionRule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Example headings must have an em-dash description.",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
heading(node) {
|
|
13
|
+
const filename = getFilename(context);
|
|
14
|
+
if (!filename.endsWith("-rule.md"))
|
|
15
|
+
return;
|
|
16
|
+
if (node.depth !== 2)
|
|
17
|
+
return;
|
|
18
|
+
const text = getTextContent(node).trim();
|
|
19
|
+
if (!/^(?:Incorrect|Correct)\b/.test(text))
|
|
20
|
+
return;
|
|
21
|
+
if (!/^(?:Incorrect|Correct) — .+/.test(text)) {
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: `example heading has no em-dash description: ${text}`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A Guide whose steps use terms that a glossary Reference defines
|
|
3
|
+
* must link that Reference from its first step.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { getFilename, getTextContent } from "./helpers.js";
|
|
8
|
+
import { findDocsRoot, getProjectDocs } from "./project-index.js";
|
|
9
|
+
/** Extract glossary terms from a reference document. */
|
|
10
|
+
function extractGlossaryTerms(filePath) {
|
|
11
|
+
const content = readFileSync(filePath, "utf8");
|
|
12
|
+
const terms = [];
|
|
13
|
+
// Look for definition-style patterns: "## Term" headings
|
|
14
|
+
const headingPattern = /^## (?<term>.+)$/gm;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = headingPattern.exec(content)) !== null) {
|
|
17
|
+
const term = match.groups?.term?.trim();
|
|
18
|
+
if (term)
|
|
19
|
+
terms.push(term);
|
|
20
|
+
}
|
|
21
|
+
return terms;
|
|
22
|
+
}
|
|
23
|
+
/** Collect the text of every step, and the doc links of the first step. */
|
|
24
|
+
function collectSteps(node) {
|
|
25
|
+
const texts = [];
|
|
26
|
+
const firstStepLinks = [];
|
|
27
|
+
if (node.type === "list" && node.ordered) {
|
|
28
|
+
for (const item of node.children) {
|
|
29
|
+
texts.push(getTextContent(item));
|
|
30
|
+
if (firstStepLinks.length === 0) {
|
|
31
|
+
collectDocLinks(item, firstStepLinks);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if ("children" in node) {
|
|
36
|
+
for (const child of node.children) {
|
|
37
|
+
const nested = collectSteps(child);
|
|
38
|
+
texts.push(...nested.texts);
|
|
39
|
+
if (firstStepLinks.length === 0)
|
|
40
|
+
firstStepLinks.push(...nested.firstStepLinks);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { texts, firstStepLinks };
|
|
44
|
+
}
|
|
45
|
+
function collectDocLinks(node, out) {
|
|
46
|
+
if (node.type === "link" && node.url.endsWith(".md"))
|
|
47
|
+
out.push(node.url);
|
|
48
|
+
if ("children" in node) {
|
|
49
|
+
for (const child of node.children)
|
|
50
|
+
collectDocLinks(child, out);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const glossaryTermLinkingRule = {
|
|
54
|
+
meta: {
|
|
55
|
+
type: "problem",
|
|
56
|
+
docs: {
|
|
57
|
+
description: "A Guide whose steps use glossary terms must link that Reference from its first step.",
|
|
58
|
+
recommended: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
return {
|
|
63
|
+
root(node) {
|
|
64
|
+
const filename = getFilename(context);
|
|
65
|
+
if (!filename.endsWith("-guide.md"))
|
|
66
|
+
return;
|
|
67
|
+
const docsRoot = findDocsRoot(filename);
|
|
68
|
+
if (!docsRoot)
|
|
69
|
+
return;
|
|
70
|
+
const docs = getProjectDocs(docsRoot);
|
|
71
|
+
const guideDir = path.dirname(filename);
|
|
72
|
+
// Find reference documents owned by this guide
|
|
73
|
+
const guideReferences = docs.filter((doc) => doc.kind === "reference" && path.dirname(doc.filePath) === guideDir);
|
|
74
|
+
if (guideReferences.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
// Collect glossary terms from owned references
|
|
77
|
+
const glossaryTerms = [];
|
|
78
|
+
for (const ref of guideReferences) {
|
|
79
|
+
glossaryTerms.push(...extractGlossaryTerms(ref.filePath));
|
|
80
|
+
}
|
|
81
|
+
if (glossaryTerms.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const { texts: stepTexts, firstStepLinks } = collectSteps(node);
|
|
84
|
+
// Check if any step uses glossary terms
|
|
85
|
+
const usedTerms = glossaryTerms.filter((term) => stepTexts.some((text) => text.includes(term)));
|
|
86
|
+
if (usedTerms.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
// Check if the first step links to any owned reference
|
|
89
|
+
const hasRefLink = firstStepLinks.some((link) => guideReferences.some((ref) => link.includes(ref.fileName)));
|
|
90
|
+
if (!hasRefLink) {
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
message: `guide uses glossary terms (${usedTerms.join(", ")}) but first step does not link the reference`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Guide folders must have an entry point.
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getFilename } from "./helpers.js";
|
|
6
|
+
import { findDocsRoot, getProjectDocs } from "./project-index.js";
|
|
7
|
+
export const guideFolderEntryPointRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Guide folders must have an entry point.",
|
|
12
|
+
recommended: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
root(node) {
|
|
18
|
+
const filename = getFilename(context);
|
|
19
|
+
if (!filename.endsWith(".md"))
|
|
20
|
+
return;
|
|
21
|
+
const docsRoot = findDocsRoot(filename);
|
|
22
|
+
if (!docsRoot)
|
|
23
|
+
return;
|
|
24
|
+
const docs = getProjectDocs(docsRoot);
|
|
25
|
+
// Find guide folders: folders that hold rules/ or references/
|
|
26
|
+
const guideFolders = new Set(docs
|
|
27
|
+
.filter((doc) => ["rules", "references"].includes(path.basename(path.dirname(doc.filePath))))
|
|
28
|
+
.map((doc) => path.dirname(path.dirname(doc.filePath)))
|
|
29
|
+
// A rules/ or references/ folder directly under docs root belongs to no guide
|
|
30
|
+
.filter((folder) => path.resolve(folder) !== path.resolve(docsRoot)));
|
|
31
|
+
// Check if current file is in a guide folder that needs an entry point
|
|
32
|
+
const currentDir = path.dirname(filename);
|
|
33
|
+
if (guideFolders.has(currentDir)) {
|
|
34
|
+
const expectedEntryPoint = `${path.basename(currentDir)}.md`;
|
|
35
|
+
const hasEntryPoint = docs.some((doc) => doc.kind === "guide" && path.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint);
|
|
36
|
+
if (!hasEntryPoint) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
message: `folder holds support documents but has no ${expectedEntryPoint} entry point`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getFilename } from "./helpers.js";
|
|
2
|
+
/** Visit every ordered-list item and run a check. */
|
|
3
|
+
function visitSteps(node, check) {
|
|
4
|
+
if (node.type === "list" && node.ordered) {
|
|
5
|
+
for (const child of node.children)
|
|
6
|
+
check(child);
|
|
7
|
+
}
|
|
8
|
+
if ("children" in node) {
|
|
9
|
+
for (const child of node.children)
|
|
10
|
+
visitSteps(child, check);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Collect guide links in a subtree. */
|
|
14
|
+
function collectGuideLinks(node) {
|
|
15
|
+
if (node.type === "link" && node.url.endsWith("-guide.md"))
|
|
16
|
+
return [node];
|
|
17
|
+
if ("children" in node) {
|
|
18
|
+
return node.children.flatMap(collectGuideLinks);
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
export const guideLinkAnchorsRule = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "A step that links another Guide must link directly to a How To section.",
|
|
27
|
+
recommended: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
root(node) {
|
|
33
|
+
const filename = getFilename(context);
|
|
34
|
+
if (!filename.endsWith("-guide.md"))
|
|
35
|
+
return;
|
|
36
|
+
visitSteps(node, (item) => {
|
|
37
|
+
for (const link of collectGuideLinks(item)) {
|
|
38
|
+
// A guide link should have an anchor (#) pointing to a section.
|
|
39
|
+
if (!link.url.includes("#")) {
|
|
40
|
+
context.report({
|
|
41
|
+
node: link,
|
|
42
|
+
message: `guide link ${link.url} does not point to a specific section`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getFilename, getLine, getTextContent } from "./helpers.js";
|
|
2
|
+
function containsLink(node) {
|
|
3
|
+
if (node.type === "link")
|
|
4
|
+
return true;
|
|
5
|
+
if ("children" in node) {
|
|
6
|
+
return node.children.some(containsLink);
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
export const guideOverviewNoLinksRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Guide overview must not link to other documents.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
root(node) {
|
|
21
|
+
const filename = getFilename(context);
|
|
22
|
+
if (!filename.endsWith("-guide.md"))
|
|
23
|
+
return;
|
|
24
|
+
let titleLine = 0;
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
if (child.type === "heading" && child.depth === 1) {
|
|
27
|
+
titleLine = getLine(child);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!titleLine)
|
|
32
|
+
return;
|
|
33
|
+
// Find the first content paragraph after the title (the overview).
|
|
34
|
+
for (const child of node.children) {
|
|
35
|
+
if (child.type !== "paragraph" || getLine(child) <= titleLine)
|
|
36
|
+
continue;
|
|
37
|
+
const text = getTextContent(child).trim();
|
|
38
|
+
if (!text || text.startsWith("#"))
|
|
39
|
+
continue;
|
|
40
|
+
if (containsLink(child)) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: child,
|
|
43
|
+
message: "guide overview links another document",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { containsRfcKeyword, getFilename, getTextContent } from "./helpers.js";
|
|
2
|
+
export const guideStatesNoRequirementRule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Guide must not state requirements with RFC 2119 vocabulary.",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
listItem(node) {
|
|
13
|
+
const filename = getFilename(context);
|
|
14
|
+
if (!filename.endsWith("-guide.md"))
|
|
15
|
+
return;
|
|
16
|
+
const text = getTextContent(node);
|
|
17
|
+
if (containsRfcKeyword(text)) {
|
|
18
|
+
context.report({
|
|
19
|
+
node,
|
|
20
|
+
message: "guide states a requirement with RFC 2119 vocabulary",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getFilename } from "./helpers.js";
|
|
2
|
+
/** Count document links in a subtree. */
|
|
3
|
+
function countDocLinks(node) {
|
|
4
|
+
if (node.type === "link" && node.url.endsWith(".md"))
|
|
5
|
+
return 1;
|
|
6
|
+
if ("children" in node) {
|
|
7
|
+
return node.children.reduce((sum, child) => sum + countDocLinks(child), 0);
|
|
8
|
+
}
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
/** Visit every ordered-list item and run a check. */
|
|
12
|
+
function visitSteps(node, check) {
|
|
13
|
+
if (node.type === "list" && node.ordered) {
|
|
14
|
+
for (const child of node.children)
|
|
15
|
+
check(child);
|
|
16
|
+
}
|
|
17
|
+
if ("children" in node) {
|
|
18
|
+
for (const child of node.children)
|
|
19
|
+
visitSteps(child, check);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const guideStepSingleLinkRule = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Each guide step must link at most one document total.",
|
|
27
|
+
recommended: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
root(node) {
|
|
33
|
+
const filename = getFilename(context);
|
|
34
|
+
if (!filename.endsWith("-guide.md"))
|
|
35
|
+
return;
|
|
36
|
+
visitSteps(node, (item) => {
|
|
37
|
+
const links = countDocLinks(item);
|
|
38
|
+
if (links > 1) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: item,
|
|
41
|
+
message: `step links ${String(links)} documents`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getFilename, getTextContent } from "./helpers.js";
|
|
2
|
+
function countSentences(text) {
|
|
3
|
+
return text.split(/[.!?](?:\s+|$)/).filter((part) => part.trim() !== "").length;
|
|
4
|
+
}
|
|
5
|
+
export const guideStepSingleSentenceRule = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Each guide step must be concise and use one sentence.",
|
|
10
|
+
recommended: true,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
// Walked list context, tracked via enter/exit so listItem can see its
|
|
15
|
+
// enclosing list without relying on a second visitor argument.
|
|
16
|
+
const listsOnPath = [];
|
|
17
|
+
return {
|
|
18
|
+
list(node) {
|
|
19
|
+
listsOnPath.push(node);
|
|
20
|
+
},
|
|
21
|
+
"list:exit"() {
|
|
22
|
+
listsOnPath.pop();
|
|
23
|
+
},
|
|
24
|
+
listItem(node) {
|
|
25
|
+
const filename = getFilename(context);
|
|
26
|
+
if (!filename.endsWith("-guide.md"))
|
|
27
|
+
return;
|
|
28
|
+
// Steps are ordered list items.
|
|
29
|
+
if (!listsOnPath.at(-1)?.ordered)
|
|
30
|
+
return;
|
|
31
|
+
const text = getTextContent(node).trim();
|
|
32
|
+
const sentenceCount = countSentences(text);
|
|
33
|
+
if (sentenceCount > 1) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
message: `step uses ${String(sentenceCount)} sentences`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for markdown ESLint rules operating on @eslint/markdown's AST.
|
|
3
|
+
*/
|
|
4
|
+
import type { Nodes } from "mdast";
|
|
5
|
+
/**
|
|
6
|
+
* Check if text contains RFC 2119 vocabulary.
|
|
7
|
+
* Returns the first keyword found, or null.
|
|
8
|
+
*/
|
|
9
|
+
export declare function containsRfcKeyword(text: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Get the file path being linted.
|
|
12
|
+
*
|
|
13
|
+
* Accepts the rule context structurally so any language plugin can pass it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getFilename(context: {
|
|
16
|
+
filename: string;
|
|
17
|
+
}): string;
|
|
18
|
+
/**
|
|
19
|
+
* Get the text content of an mdast node.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTextContent(node: Nodes): string;
|
|
22
|
+
/**
|
|
23
|
+
* Get the line number from a node's position.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getLine(node: Nodes): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 2119 keywords matched by word-boundary regex, longest first.
|
|
3
|
+
*/
|
|
4
|
+
const RFC_PATTERN = /\b(?<keyword>MUST NOT|MUST|SHALL NOT|SHALL|SHOULD NOT|SHOULD|MAY|RECOMMENDED|NOT RECOMMENDED|OPTIONAL|REQUIRED)\b/;
|
|
5
|
+
/**
|
|
6
|
+
* Check if text contains RFC 2119 vocabulary.
|
|
7
|
+
* Returns the first keyword found, or null.
|
|
8
|
+
*/
|
|
9
|
+
export function containsRfcKeyword(text) {
|
|
10
|
+
const match = RFC_PATTERN.exec(text);
|
|
11
|
+
return match?.groups?.keyword ?? null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the file path being linted.
|
|
15
|
+
*
|
|
16
|
+
* Accepts the rule context structurally so any language plugin can pass it.
|
|
17
|
+
*/
|
|
18
|
+
export function getFilename(context) {
|
|
19
|
+
return context.filename;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the text content of an mdast node.
|
|
23
|
+
*/
|
|
24
|
+
export function getTextContent(node) {
|
|
25
|
+
if ("value" in node && typeof node.value === "string")
|
|
26
|
+
return node.value;
|
|
27
|
+
if ("children" in node) {
|
|
28
|
+
return node.children.map(getTextContent).join("");
|
|
29
|
+
}
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the line number from a node's position.
|
|
34
|
+
*/
|
|
35
|
+
export function getLine(node) {
|
|
36
|
+
return node.position?.start.line ?? 0;
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const mdRules: {
|
|
2
|
+
"doc-kind-suffix": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
3
|
+
"title-matches-file-name": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
4
|
+
"overview-present": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
5
|
+
"overview-length": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
6
|
+
"guide-overview-no-links": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
7
|
+
"guide-step-single-sentence": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
8
|
+
"guide-step-single-link": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
9
|
+
"guide-states-no-requirement": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
10
|
+
"requirement-present": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
11
|
+
"rule-paired-examples": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
12
|
+
"example-heading-description": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
13
|
+
"policy-no-examples": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
14
|
+
"policy-single-document": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
15
|
+
"no-cross-document-link": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
16
|
+
"reference-no-rfc-vocabulary": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
17
|
+
"reference-block-headings": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
18
|
+
"support-document-placement": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
19
|
+
"no-template-prompt": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
20
|
+
"guide-folder-entry-point": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
21
|
+
"rfc-only-in-bullets": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
22
|
+
"policy-subject-headings": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
23
|
+
"guide-link-anchors": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
24
|
+
"no-nested-how-to": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
25
|
+
"glossary-term-linking": import("@eslint/markdown").MarkdownRuleDefinition;
|
|
26
|
+
};
|
|
27
|
+
export type MdRuleName = keyof typeof mdRules;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview All markdown ESLint rules for documentation checks.
|
|
3
|
+
*/
|
|
4
|
+
import { docKindSuffixRule } from "./doc-kind-suffix.js";
|
|
5
|
+
import { titleMatchesFileNameRule } from "./title-matches-file-name.js";
|
|
6
|
+
import { overviewPresentRule } from "./overview-present.js";
|
|
7
|
+
import { overviewLengthRule } from "./overview-length.js";
|
|
8
|
+
import { guideOverviewNoLinksRule } from "./guide-overview-no-links.js";
|
|
9
|
+
import { guideStepSingleSentenceRule } from "./guide-step-single-sentence.js";
|
|
10
|
+
import { guideStepSingleLinkRule } from "./guide-step-single-link.js";
|
|
11
|
+
import { guideStatesNoRequirementRule } from "./guide-states-no-requirement.js";
|
|
12
|
+
import { requirementPresentRule } from "./requirement-present.js";
|
|
13
|
+
import { rulePairedExamplesRule } from "./rule-paired-examples.js";
|
|
14
|
+
import { exampleHeadingDescriptionRule } from "./example-heading-description.js";
|
|
15
|
+
import { policyNoExamplesRule } from "./policy-no-examples.js";
|
|
16
|
+
import { policySingleDocumentRule } from "./policy-single-document.js";
|
|
17
|
+
import { noCrossDocumentLinkRule } from "./no-cross-document-link.js";
|
|
18
|
+
import { referenceNoRfcVocabularyRule } from "./reference-no-rfc-vocabulary.js";
|
|
19
|
+
import { referenceBlockHeadingsRule } from "./reference-block-headings.js";
|
|
20
|
+
import { supportDocumentPlacementRule } from "./support-document-placement.js";
|
|
21
|
+
import { noTemplatePromptRule } from "./no-template-prompt.js";
|
|
22
|
+
import { guideFolderEntryPointRule } from "./guide-folder-entry-point.js";
|
|
23
|
+
import { rfcOnlyInBulletsRule } from "./rfc-only-in-bullets.js";
|
|
24
|
+
import { policySubjectHeadingsRule } from "./policy-subject-headings.js";
|
|
25
|
+
import { guideLinkAnchorsRule } from "./guide-link-anchors.js";
|
|
26
|
+
import { noNestedHowToRule } from "./no-nested-how-to.js";
|
|
27
|
+
import { glossaryTermLinkingRule } from "./glossary-term-linking.js";
|
|
28
|
+
export const mdRules = {
|
|
29
|
+
"doc-kind-suffix": docKindSuffixRule,
|
|
30
|
+
"title-matches-file-name": titleMatchesFileNameRule,
|
|
31
|
+
"overview-present": overviewPresentRule,
|
|
32
|
+
"overview-length": overviewLengthRule,
|
|
33
|
+
"guide-overview-no-links": guideOverviewNoLinksRule,
|
|
34
|
+
"guide-step-single-sentence": guideStepSingleSentenceRule,
|
|
35
|
+
"guide-step-single-link": guideStepSingleLinkRule,
|
|
36
|
+
"guide-states-no-requirement": guideStatesNoRequirementRule,
|
|
37
|
+
"requirement-present": requirementPresentRule,
|
|
38
|
+
"rule-paired-examples": rulePairedExamplesRule,
|
|
39
|
+
"example-heading-description": exampleHeadingDescriptionRule,
|
|
40
|
+
"policy-no-examples": policyNoExamplesRule,
|
|
41
|
+
"policy-single-document": policySingleDocumentRule,
|
|
42
|
+
"no-cross-document-link": noCrossDocumentLinkRule,
|
|
43
|
+
"reference-no-rfc-vocabulary": referenceNoRfcVocabularyRule,
|
|
44
|
+
"reference-block-headings": referenceBlockHeadingsRule,
|
|
45
|
+
"support-document-placement": supportDocumentPlacementRule,
|
|
46
|
+
"no-template-prompt": noTemplatePromptRule,
|
|
47
|
+
"guide-folder-entry-point": guideFolderEntryPointRule,
|
|
48
|
+
"rfc-only-in-bullets": rfcOnlyInBulletsRule,
|
|
49
|
+
"policy-subject-headings": policySubjectHeadingsRule,
|
|
50
|
+
"guide-link-anchors": guideLinkAnchorsRule,
|
|
51
|
+
"no-nested-how-to": noNestedHowToRule,
|
|
52
|
+
"glossary-term-linking": glossaryTermLinkingRule,
|
|
53
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getFilename } from "./helpers.js";
|
|
2
|
+
/** The document kind for a filename, when the no-link rule applies to it. */
|
|
3
|
+
function linkedKind(filename) {
|
|
4
|
+
if (filename.endsWith("-rule.md"))
|
|
5
|
+
return "rule";
|
|
6
|
+
if (filename.endsWith("-reference.md"))
|
|
7
|
+
return "reference";
|
|
8
|
+
if (filename.endsWith("-policy.md"))
|
|
9
|
+
return "policy";
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
export const noCrossDocumentLinkRule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Rules, references, and policy must not link to other documents.",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
link(node) {
|
|
23
|
+
const filename = getFilename(context);
|
|
24
|
+
const kind = linkedKind(filename);
|
|
25
|
+
if (!kind)
|
|
26
|
+
return;
|
|
27
|
+
if (node.url.endsWith(".md")) {
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
message: `${kind} links another document: ${node.url}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|