pasika 0.3.1 → 0.3.3
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 +67 -9
- package/dist/cli/index.js +34 -40
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +11 -0
- package/dist/enforcement/coverage.d.ts +5 -1
- package/dist/enforcement/coverage.js +68 -39
- package/dist/enforcement/doctor.d.ts +16 -0
- package/dist/enforcement/doctor.js +249 -0
- package/dist/enforcement/parse-docs.js +5 -1
- 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/package.json +14 -7
- package/dist/enforcement/docs-check.d.ts +0 -17
- package/dist/enforcement/docs-check.js +0 -162
- package/enforcement/registry.json +0 -1139
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DocMeta {
|
|
2
|
+
/** Absolute path on disk. */
|
|
3
|
+
filePath: string;
|
|
4
|
+
/** Path relative to docs root. */
|
|
5
|
+
doc: string;
|
|
6
|
+
fileName: string;
|
|
7
|
+
kind?: "guide" | "rule" | "reference" | "policy";
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get all documents in the docs folder. Results are cached per docsRoot.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getProjectDocs(docsRoot: string): DocMeta[];
|
|
14
|
+
/**
|
|
15
|
+
* Try to find the docsRoot from a file path by walking up to find the docs/ directory.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findDocsRoot(filePath: string): string | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared project index for cross-document markdown rules.
|
|
3
|
+
* Scans the docs folder and caches document metadata.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
const KIND_BY_SUFFIX = [
|
|
8
|
+
["-rule.md", "rule"],
|
|
9
|
+
["-guide.md", "guide"],
|
|
10
|
+
["-reference.md", "reference"],
|
|
11
|
+
["-policy.md", "policy"],
|
|
12
|
+
];
|
|
13
|
+
function listMarkdownFiles(dir) {
|
|
14
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
15
|
+
const entryPath = path.join(dir, entry);
|
|
16
|
+
if (statSync(entryPath).isDirectory()) {
|
|
17
|
+
return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
|
|
18
|
+
}
|
|
19
|
+
return entry.endsWith(".md") ? [entryPath] : [];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function extractTitle(filePath) {
|
|
23
|
+
const content = readFileSync(filePath, "utf8");
|
|
24
|
+
const match = /^# (?<title>.+)$/m.exec(content);
|
|
25
|
+
return match?.groups?.title?.trim() ?? "";
|
|
26
|
+
}
|
|
27
|
+
// Memoized cache keyed by docsRoot
|
|
28
|
+
const cache = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Get all documents in the docs folder. Results are cached per docsRoot.
|
|
31
|
+
*/
|
|
32
|
+
export function getProjectDocs(docsRoot) {
|
|
33
|
+
const cached = cache.get(docsRoot);
|
|
34
|
+
if (cached)
|
|
35
|
+
return cached;
|
|
36
|
+
const files = listMarkdownFiles(docsRoot);
|
|
37
|
+
const docs = files
|
|
38
|
+
.sort((a, b) => a.localeCompare(b))
|
|
39
|
+
.map((filePath) => {
|
|
40
|
+
const fileName = path.basename(filePath);
|
|
41
|
+
const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
42
|
+
return {
|
|
43
|
+
filePath,
|
|
44
|
+
doc: path.relative(docsRoot, filePath).split(path.sep).join("/"),
|
|
45
|
+
fileName,
|
|
46
|
+
kind,
|
|
47
|
+
title: extractTitle(filePath),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
cache.set(docsRoot, docs);
|
|
51
|
+
return docs;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Try to find the docsRoot from a file path by walking up to find the docs/ directory.
|
|
55
|
+
*/
|
|
56
|
+
export function findDocsRoot(filePath) {
|
|
57
|
+
let dir = path.dirname(filePath);
|
|
58
|
+
for (;;) {
|
|
59
|
+
if (path.basename(dir) === "docs" && statSync(dir).isDirectory()) {
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir)
|
|
64
|
+
return undefined;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reference block headings rule.
|
|
3
|
+
* A Reference with a single lookup block must not add a section heading for it.
|
|
4
|
+
*/
|
|
5
|
+
import type { MarkdownRuleDefinition } from "@eslint/markdown";
|
|
6
|
+
export declare const referenceBlockHeadingsRule: MarkdownRuleDefinition;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getFilename } from "./helpers.js";
|
|
2
|
+
export const referenceBlockHeadingsRule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Reference block headings rule.",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
root(node) {
|
|
13
|
+
const filename = getFilename(context);
|
|
14
|
+
if (!filename.endsWith("-reference.md"))
|
|
15
|
+
return;
|
|
16
|
+
const sectionHeadings = [];
|
|
17
|
+
for (const child of node.children) {
|
|
18
|
+
if (child.type === "heading" && child.depth >= 2) {
|
|
19
|
+
sectionHeadings.push(child);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (sectionHeadings.length === 1) {
|
|
23
|
+
context.report({
|
|
24
|
+
node: node.children[0] ?? node,
|
|
25
|
+
message: "reference has exactly one section heading, so either a single block is headed or a first block is not",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reference documents must not use RFC 2119 vocabulary in prose.
|
|
3
|
+
* Only naming a keyword in code spans is permitted.
|
|
4
|
+
*/
|
|
5
|
+
import type { MarkdownRuleDefinition } from "@eslint/markdown";
|
|
6
|
+
export declare const referenceNoRfcVocabularyRule: MarkdownRuleDefinition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { containsRfcKeyword, getFilename } from "./helpers.js";
|
|
2
|
+
export const referenceNoRfcVocabularyRule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Reference documents must not use RFC 2119 vocabulary (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY) in prose.",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
text(node) {
|
|
13
|
+
const filename = getFilename(context);
|
|
14
|
+
if (!filename.endsWith("-reference.md"))
|
|
15
|
+
return;
|
|
16
|
+
const keyword = containsRfcKeyword(node.value);
|
|
17
|
+
if (keyword) {
|
|
18
|
+
context.report({
|
|
19
|
+
node,
|
|
20
|
+
message: `reference uses RFC 2119 vocabulary: ${keyword}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { containsRfcKeyword, getFilename, getTextContent } from "./helpers.js";
|
|
2
|
+
/** Check whether a subtree contains a bullet with RFC vocabulary. */
|
|
3
|
+
function hasRequirement(node) {
|
|
4
|
+
if (node.type === "listItem") {
|
|
5
|
+
if (containsRfcKeyword(getTextContent(node)))
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if ("children" in node) {
|
|
9
|
+
for (const child of node.children) {
|
|
10
|
+
if (hasRequirement(child))
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
export const requirementPresentRule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Rule and policy documents must state at least one requirement.",
|
|
21
|
+
recommended: true,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
root(node) {
|
|
27
|
+
const filename = getFilename(context);
|
|
28
|
+
if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!hasRequirement(node)) {
|
|
32
|
+
const kind = filename.endsWith("-rule.md") ? "rule" : "policy";
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
message: `${kind} document states no requirement`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { containsRfcKeyword, getFilename } from "./helpers.js";
|
|
2
|
+
/** Check whether a subtree contains a text node outside a bullet using RFC vocabulary. */
|
|
3
|
+
function checkOutsideBullets(node, context, insideBullet) {
|
|
4
|
+
if (node.type === "listItem") {
|
|
5
|
+
// Everything under a list item is a bullet; stop descending for the
|
|
6
|
+
// outside-bullet check, but nested lists still need walking.
|
|
7
|
+
for (const child of node.children) {
|
|
8
|
+
checkOutsideBullets(child, context, true);
|
|
9
|
+
}
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (node.type === "text") {
|
|
13
|
+
const keyword = containsRfcKeyword(node.value);
|
|
14
|
+
if (keyword && !insideBullet) {
|
|
15
|
+
context.report({
|
|
16
|
+
node,
|
|
17
|
+
message: `RFC 2119 vocabulary "${keyword}" appears outside a bullet point`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if ("children" in node) {
|
|
23
|
+
for (const child of node.children) {
|
|
24
|
+
checkOutsideBullets(child, context, insideBullet);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const rfcOnlyInBulletsRule = {
|
|
29
|
+
meta: {
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: {
|
|
32
|
+
description: "RFC 2119 vocabulary must appear only in bullet points.",
|
|
33
|
+
recommended: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
return {
|
|
38
|
+
root(node) {
|
|
39
|
+
const filename = getFilename(context);
|
|
40
|
+
if (!filename.endsWith("-rule.md"))
|
|
41
|
+
return;
|
|
42
|
+
checkOutsideBullets(node, context, false);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getFilename, getLine, getTextContent } from "./helpers.js";
|
|
2
|
+
export const rulePairedExamplesRule = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Rule must have paired Incorrect/Correct examples.",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
root(node) {
|
|
13
|
+
const filename = getFilename(context);
|
|
14
|
+
if (!filename.endsWith("-rule.md"))
|
|
15
|
+
return;
|
|
16
|
+
const exampleHeadings = [];
|
|
17
|
+
for (const child of node.children) {
|
|
18
|
+
if (child.type === "heading" && child.depth === 2) {
|
|
19
|
+
const text = getTextContent(child).trim();
|
|
20
|
+
if (/^(?:Incorrect|Correct)\b/.test(text)) {
|
|
21
|
+
exampleHeadings.push({ line: getLine(child), text });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const incorrect = exampleHeadings.filter((h) => h.text.startsWith("Incorrect"));
|
|
26
|
+
const correct = exampleHeadings.filter((h) => h.text.startsWith("Correct"));
|
|
27
|
+
if (incorrect.length === 0 || incorrect.length !== correct.length) {
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
message: `${String(incorrect.length)} Incorrect and ${String(correct.length)} Correct examples`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RuleTester wiring for the markdown rule tests.
|
|
3
|
+
*
|
|
4
|
+
* Each `describe` title in a rule test is the exact text of the requirement the
|
|
5
|
+
* case pins, which is how `pasika coverage` verifies that a requirement recorded
|
|
6
|
+
* as lint-enforced has a test behind it.
|
|
7
|
+
*/
|
|
8
|
+
import { describe } from "node:test";
|
|
9
|
+
import { RuleTester } from "eslint";
|
|
10
|
+
export declare const mdRuleTester: RuleTester;
|
|
11
|
+
export { describe };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RuleTester wiring for the markdown rule tests.
|
|
3
|
+
*
|
|
4
|
+
* Each `describe` title in a rule test is the exact text of the requirement the
|
|
5
|
+
* case pins, which is how `pasika coverage` verifies that a requirement recorded
|
|
6
|
+
* as lint-enforced has a test behind it.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import { RuleTester } from "eslint";
|
|
10
|
+
import markdown from "@eslint/markdown";
|
|
11
|
+
import { mdRules } from "../../index.js";
|
|
12
|
+
RuleTester.describe = describe;
|
|
13
|
+
RuleTester.it = it;
|
|
14
|
+
export const mdRuleTester = new RuleTester({
|
|
15
|
+
language: "markdown/gfm",
|
|
16
|
+
plugins: {
|
|
17
|
+
markdown,
|
|
18
|
+
pasika: { rules: mdRules },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
export { describe };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rules must live in rules/, references must live in references/.
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getFilename } from "./helpers.js";
|
|
6
|
+
export const supportDocumentPlacementRule = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Rules must live in rules/, references must live in references/.",
|
|
11
|
+
recommended: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
root(node) {
|
|
17
|
+
const filename = getFilename(context);
|
|
18
|
+
if (!filename.endsWith(".md"))
|
|
19
|
+
return;
|
|
20
|
+
const parentFolder = path.basename(path.dirname(filename));
|
|
21
|
+
if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: `rule lives in "${parentFolder}/" instead of "rules/"`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (filename.endsWith("-reference.md") && parentFolder !== "references") {
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
message: `reference lives in "${parentFolder}/" instead of "references/"`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Document title must match the file name in kebab-case.
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getFilename, getTextContent } from "./helpers.js";
|
|
6
|
+
function toExpectedFileName(title) {
|
|
7
|
+
return `${title
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-|-$/g, "")}.md`;
|
|
11
|
+
}
|
|
12
|
+
export const titleMatchesFileNameRule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Document title must match the file name in kebab-case.",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
heading(node) {
|
|
23
|
+
if (node.depth !== 1)
|
|
24
|
+
return;
|
|
25
|
+
const filename = getFilename(context);
|
|
26
|
+
if (!filename.endsWith(".md"))
|
|
27
|
+
return;
|
|
28
|
+
const title = getTextContent(node).trim();
|
|
29
|
+
const expectedFileName = toExpectedFileName(title);
|
|
30
|
+
const actualFileName = path.basename(filename);
|
|
31
|
+
if (!title) {
|
|
32
|
+
context.report({
|
|
33
|
+
node,
|
|
34
|
+
message: "document has no `# Title` heading",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (expectedFileName !== actualFileName) {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
message: `title "${title}" expects file name ${expectedFileName}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/named-exports
|
|
3
|
+
*
|
|
4
|
+
* Enforces named exports for application files while allowing framework routing
|
|
5
|
+
* files such as Next.js page.tsx and layout.tsx to use their required default
|
|
6
|
+
* export contract.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/code-organization-guide/rules/exports-and-imports-rule.md
|
|
9
|
+
*/
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
const FRAMEWORK_DEFAULT_EXPORT_FILES = new Set([
|
|
12
|
+
"default",
|
|
13
|
+
"error",
|
|
14
|
+
"layout",
|
|
15
|
+
"loading",
|
|
16
|
+
"not-found",
|
|
17
|
+
"page",
|
|
18
|
+
"route",
|
|
19
|
+
"template",
|
|
20
|
+
]);
|
|
21
|
+
function isFrameworkDefaultExportFile(filename) {
|
|
22
|
+
const normalized = filename.replaceAll(path.sep, "/");
|
|
23
|
+
if (!normalized.includes("/src/app/"))
|
|
24
|
+
return false;
|
|
25
|
+
const basename = path.basename(filename, path.extname(filename));
|
|
26
|
+
return FRAMEWORK_DEFAULT_EXPORT_FILES.has(basename);
|
|
27
|
+
}
|
|
28
|
+
export const namedExportsRule = {
|
|
29
|
+
meta: {
|
|
30
|
+
schema: [],
|
|
31
|
+
type: "problem",
|
|
32
|
+
docs: {
|
|
33
|
+
description: "Require named exports except where a framework requires a default export.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
if (isFrameworkDefaultExportFile(context.filename))
|
|
38
|
+
return {};
|
|
39
|
+
return {
|
|
40
|
+
ExportDefaultDeclaration(node) {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
message: "Files that export values must use named exports unless a framework or third-party package requires a different export style for that file. " +
|
|
44
|
+
"See docs/code-organization-guide/rules/exports-and-imports-rule.md",
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-eslint-disable
|
|
3
|
+
*
|
|
4
|
+
* Code MUST NOT use eslint-disable directives, and a reported violation MUST
|
|
5
|
+
* be fixed instead.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/agent-policy.md
|
|
8
|
+
*/
|
|
9
|
+
import type { Rule } from "eslint";
|
|
10
|
+
export declare const noEslintDisableRule: Rule.RuleModule;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-eslint-disable
|
|
3
|
+
*
|
|
4
|
+
* Code MUST NOT use eslint-disable directives, and a reported violation MUST
|
|
5
|
+
* be fixed instead.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/agent-policy.md
|
|
8
|
+
*/
|
|
9
|
+
export const noEslintDisableRule = {
|
|
10
|
+
meta: {
|
|
11
|
+
schema: [],
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Forbid eslint-disable directives.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
Program() {
|
|
20
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
21
|
+
if (!comment.value.includes("eslint-disable"))
|
|
22
|
+
continue;
|
|
23
|
+
if (!comment.loc)
|
|
24
|
+
continue;
|
|
25
|
+
context.report({
|
|
26
|
+
loc: comment.loc,
|
|
27
|
+
message: "Do not use eslint-disable directives; fix the reported violation instead.",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -5,32 +5,22 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Return the name of an export node so we can report it clearly.
|
|
10
|
-
* Handles:
|
|
11
|
-
* export function Foo() { }
|
|
12
|
-
* export const Foo = () => { }
|
|
13
|
-
* export const Foo = function () { }
|
|
14
|
-
* export default function Foo() { }
|
|
15
|
-
* export default () => { }
|
|
16
|
-
*/
|
|
17
|
-
// biome-ignore lint/suspicious/noExplicitAny: AST node type from ESLint with dynamic property access
|
|
18
8
|
function getExportName(declaration) {
|
|
19
|
-
if (declaration
|
|
20
|
-
return declaration.id.name;
|
|
9
|
+
if (declaration?.type === "FunctionDeclaration" && declaration.id) {
|
|
10
|
+
return declaration.id.name ?? null;
|
|
21
11
|
}
|
|
22
|
-
if (declaration
|
|
23
|
-
const declarator = declaration.declarations[0];
|
|
24
|
-
if (declarator?.id
|
|
12
|
+
if (declaration?.type === "VariableDeclaration") {
|
|
13
|
+
const declarator = declaration.declarations?.[0];
|
|
14
|
+
if (declarator?.id?.type === "Identifier" &&
|
|
25
15
|
(declarator.init?.type === "ArrowFunctionExpression" || declarator.init?.type === "FunctionExpression")) {
|
|
26
|
-
return declarator.id.name;
|
|
16
|
+
return declarator.id.name ?? null;
|
|
27
17
|
}
|
|
28
18
|
}
|
|
29
|
-
if (declaration
|
|
19
|
+
if (declaration?.type === "ArrowFunctionExpression") {
|
|
30
20
|
return "default";
|
|
31
21
|
}
|
|
32
|
-
if (declaration
|
|
33
|
-
return declaration.id.name;
|
|
22
|
+
if (declaration?.type === "FunctionExpression" && declaration.id) {
|
|
23
|
+
return declaration.id.name ?? null;
|
|
34
24
|
}
|
|
35
25
|
return null;
|
|
36
26
|
}
|
|
@@ -55,8 +45,6 @@ export const noMixedConcernsRule = {
|
|
|
55
45
|
}
|
|
56
46
|
return {
|
|
57
47
|
ExportNamedDeclaration(node) {
|
|
58
|
-
if (!node.declaration)
|
|
59
|
-
return;
|
|
60
48
|
const name = getExportName(node.declaration);
|
|
61
49
|
if (name)
|
|
62
50
|
registerExport(name);
|
|
@@ -81,4 +69,3 @@ export const noMixedConcernsRule = {
|
|
|
81
69
|
};
|
|
82
70
|
},
|
|
83
71
|
};
|
|
84
|
-
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any -- re-enable after AST node access block */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveSpecifier } from "../project/index.js";
|
|
3
|
+
export const noUtilBarrelRule = {
|
|
4
|
+
meta: {
|
|
5
|
+
schema: [],
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Require utilities to be imported directly without a barrel.",
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
const filename = path.resolve(context.filename);
|
|
13
|
+
const sourceRoot = sourceRootOf(filename);
|
|
14
|
+
return {
|
|
15
|
+
Program(node) {
|
|
16
|
+
for (const specifier of importSpecifiers(context.sourceCode.text)) {
|
|
17
|
+
const target = resolveSpecifier(filename, specifier, sourceRoot);
|
|
18
|
+
if (!target)
|
|
19
|
+
continue;
|
|
20
|
+
const segments = target.replace(/\\/g, "/").split("/");
|
|
21
|
+
const utilsIndex = segments.lastIndexOf("utils");
|
|
22
|
+
if (utilsIndex < 0 || !path.basename(target).startsWith("index."))
|
|
23
|
+
continue;
|
|
24
|
+
context.report({
|
|
25
|
+
node,
|
|
26
|
+
message: `Import utilities directly instead of through "${specifier}". ` +
|
|
27
|
+
"See docs/code-organization-guide/rules/utilities-rule.md",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function importSpecifiers(source) {
|
|
35
|
+
const specifiers = [];
|
|
36
|
+
const pattern = /\bimport\s+(?:(?:type\s+)?[\s\S]*?\sfrom\s+)?["'](?<specifier>[^"']+)["']/g;
|
|
37
|
+
for (const match of source.matchAll(pattern)) {
|
|
38
|
+
const specifier = match.groups?.specifier;
|
|
39
|
+
if (specifier)
|
|
40
|
+
specifiers.push(specifier);
|
|
41
|
+
}
|
|
42
|
+
return specifiers;
|
|
43
|
+
}
|
|
44
|
+
function sourceRootOf(filename) {
|
|
45
|
+
const marker = `${path.sep}src${path.sep}`;
|
|
46
|
+
const srcIndex = filename.lastIndexOf(marker);
|
|
47
|
+
return srcIndex >= 0 ? filename.slice(0, srcIndex + marker.length - 1) : path.resolve("src");
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/pure-function-extract
|
|
3
|
+
*
|
|
4
|
+
* A pure function MUST be extracted to utils/, even when it has one consumer.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/code-organization-guide/rules/utilities-rule.md
|
|
7
|
+
*/
|
|
8
|
+
import type { Rule } from "eslint";
|
|
9
|
+
export declare const pureFunctionExtractRule: Rule.RuleModule;
|