pasika 0.3.2 → 0.3.4
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 +72 -20
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +4999 -103
- package/dist/eslint/pasika/index.d.ts +165 -53
- package/dist/eslint/pasika/index.js +4424 -89
- package/package.json +5 -6
- package/dist/cli/output.d.ts +0 -3
- package/dist/cli/output.js +0 -11
- package/dist/enforcement/coverage.d.ts +0 -51
- package/dist/enforcement/coverage.js +0 -206
- package/dist/enforcement/doctor.d.ts +0 -16
- package/dist/enforcement/doctor.js +0 -249
- package/dist/enforcement/normalize.d.ts +0 -11
- package/dist/enforcement/normalize.js +0 -21
- package/dist/enforcement/parse-docs.d.ts +0 -58
- package/dist/enforcement/parse-docs.js +0 -94
- package/dist/enforcement/types.d.ts +0 -54
- package/dist/enforcement/types.js +0 -50
- package/dist/eslint/pasika/ast-types.d.ts +0 -122
- package/dist/eslint/pasika/ast-types.js +0 -1
- package/dist/eslint/pasika/project/ccf.d.ts +0 -48
- package/dist/eslint/pasika/project/ccf.js +0 -119
- package/dist/eslint/pasika/project/index.d.ts +0 -21
- package/dist/eslint/pasika/project/index.js +0 -139
- package/dist/eslint/pasika/project/parse-module.d.ts +0 -27
- package/dist/eslint/pasika/project/parse-module.js +0 -128
- package/dist/eslint/pasika/rules/application-structure.d.ts +0 -2
- package/dist/eslint/pasika/rules/application-structure.js +0 -152
- package/dist/eslint/pasika/rules/component-conventions.d.ts +0 -16
- package/dist/eslint/pasika/rules/component-conventions.js +0 -145
- package/dist/eslint/pasika/rules/component-nesting.d.ts +0 -12
- package/dist/eslint/pasika/rules/component-nesting.js +0 -69
- package/dist/eslint/pasika/rules/component-placement.d.ts +0 -11
- package/dist/eslint/pasika/rules/component-placement.js +0 -75
- package/dist/eslint/pasika/rules/config-extraction.d.ts +0 -14
- package/dist/eslint/pasika/rules/config-extraction.js +0 -74
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +0 -2
- package/dist/eslint/pasika/rules/cross-feature-import.js +0 -75
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/apply-usage.js +0 -45
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +0 -49
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +0 -39
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +0 -52
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/global-css-location.js +0 -42
- package/dist/eslint/pasika/rules/css/helpers.d.ts +0 -29
- package/dist/eslint/pasika/rules/css/helpers.js +0 -93
- package/dist/eslint/pasika/rules/css/index.d.ts +0 -13
- package/dist/eslint/pasika/rules/css/index.js +0 -25
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/root-variables.js +0 -53
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/rule-tester.js +0 -22
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +0 -76
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/surface-utility.js +0 -41
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +0 -9
- package/dist/eslint/pasika/rules/css/theme-reset.js +0 -36
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +0 -53
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +0 -10
- package/dist/eslint/pasika/rules/cva-appearance-props.js +0 -77
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +0 -11
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +0 -97
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/data-testid-case.js +0 -76
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +0 -78
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +0 -21
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +0 -86
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +0 -86
- package/dist/eslint/pasika/rules/filename-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/filename-case.js +0 -123
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +0 -12
- package/dist/eslint/pasika/rules/hook-complexity.js +0 -114
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +0 -11
- package/dist/eslint/pasika/rules/hook-extraction.js +0 -55
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-boundaries.js +0 -167
- package/dist/eslint/pasika/rules/import-through-index.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-through-index.js +0 -52
- package/dist/eslint/pasika/rules/interactive-component.d.ts +0 -10
- package/dist/eslint/pasika/rules/interactive-component.js +0 -76
- package/dist/eslint/pasika/rules/json/index.d.ts +0 -5
- package/dist/eslint/pasika/rules/json/index.js +0 -9
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +0 -45
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +0 -41
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/json/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +0 -9
- package/dist/eslint/pasika/rules/jsx-hygiene.js +0 -108
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +0 -10
- package/dist/eslint/pasika/rules/locale-dotted-path.js +0 -65
- package/dist/eslint/pasika/rules/locale-placement.d.ts +0 -13
- package/dist/eslint/pasika/rules/locale-placement.js +0 -163
- package/dist/eslint/pasika/rules/locales-location.d.ts +0 -11
- package/dist/eslint/pasika/rules/locales-location.js +0 -56
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +0 -29
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/example-heading-description.js +0 -30
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +0 -99
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +0 -46
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +0 -50
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +0 -51
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +0 -26
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +0 -48
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +0 -42
- package/dist/eslint/pasika/rules/md/helpers.d.ts +0 -25
- package/dist/eslint/pasika/rules/md/helpers.js +0 -37
- package/dist/eslint/pasika/rules/md/index.d.ts +0 -27
- package/dist/eslint/pasika/rules/md/index.js +0 -53
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +0 -36
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +0 -39
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +0 -22
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-length.js +0 -47
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-present.js +0 -41
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +0 -28
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-single-document.js +0 -34
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +0 -58
- package/dist/eslint/pasika/rules/md/project-index.d.ts +0 -17
- package/dist/eslint/pasika/rules/md/project-index.js +0 -67
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +0 -31
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +0 -26
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/requirement-present.js +0 -41
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +0 -46
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +0 -36
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/md/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/support-document-placement.js +0 -36
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +0 -46
- package/dist/eslint/pasika/rules/named-exports.d.ts +0 -2
- package/dist/eslint/pasika/rules/named-exports.js +0 -49
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +0 -97
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +0 -10
- package/dist/eslint/pasika/rules/no-eslint-disable.js +0 -33
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +0 -71
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +0 -2
- package/dist/eslint/pasika/rules/no-util-barrel.js +0 -48
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +0 -9
- package/dist/eslint/pasika/rules/pure-function-extract.js +0 -92
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +0 -13
- package/dist/eslint/pasika/rules/shared-style-dedup.js +0 -91
- package/dist/eslint/pasika/rules/stay-flat.d.ts +0 -12
- package/dist/eslint/pasika/rules/stay-flat.js +0 -81
- package/dist/eslint/pasika/rules/support-file-placement.d.ts +0 -15
- package/dist/eslint/pasika/rules/support-file-placement.js +0 -70
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +0 -2
- package/dist/eslint/pasika/rules/support-folder-shape.js +0 -59
- package/dist/eslint/pasika/rules/type-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/type-extraction.js +0 -85
- package/dist/eslint/pasika/rules/ui-state.d.ts +0 -9
- package/dist/eslint/pasika/rules/ui-state.js +0 -99
- package/dist/eslint/pasika/rules/util-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/util-file-name.js +0 -48
- package/dist/eslint/pasika/rules/value-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/value-extraction.js +0 -48
- package/enforcement/registry.json +0 -1145
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/locale-placement
|
|
3
|
-
*
|
|
4
|
-
* Locales read by files in more than one feature folder or by src/shared/,
|
|
5
|
-
* src/compositions/, src/app/, or root support folders MUST live at the top
|
|
6
|
-
* level of locales. Locales read only by files in one feature folder MUST live
|
|
7
|
-
* in an object with the camelCase form of its feature folder name. The rule
|
|
8
|
-
* reads which keys the tree uses, because a single file cannot see them.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/code-organization-guide/rules/locales-rule.md
|
|
11
|
-
*/
|
|
12
|
-
import type { Rule } from "eslint";
|
|
13
|
-
export declare const localePlacementRule: Rule.RuleModule;
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/locale-placement
|
|
3
|
-
*
|
|
4
|
-
* Locales read by files in more than one feature folder or by src/shared/,
|
|
5
|
-
* src/compositions/, src/app/, or root support folders MUST live at the top
|
|
6
|
-
* level of locales. Locales read only by files in one feature folder MUST live
|
|
7
|
-
* in an object with the camelCase form of its feature folder name. The rule
|
|
8
|
-
* reads which keys the tree uses, because a single file cannot see them.
|
|
9
|
-
*
|
|
10
|
-
* @see docs/code-organization-guide/rules/locales-rule.md
|
|
11
|
-
*/
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
14
|
-
import ts from "typescript";
|
|
15
|
-
import { getProjectIndex, resolveSpecifier } from "../project/index.js";
|
|
16
|
-
import { describeConsumers, segmentsOf, SUPPORT_FOLDERS } from "../project/ccf.js";
|
|
17
|
-
/** The first segment of a `locales.<key>` read. */
|
|
18
|
-
const LOCALE_ACCESS = /\blocales\.(?<key>[A-Za-z_$][\w$]*)/g;
|
|
19
|
-
const camelCase = (name) => name.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase());
|
|
20
|
-
const FORCED_TOP_LEVEL = new Set(["app", "shared", "compositions", "config"]);
|
|
21
|
-
/** A file whose own folder decides the locale scope: app/shared/compositions/config or a root support folder. */
|
|
22
|
-
function forcesTopLevel(segments) {
|
|
23
|
-
return FORCED_TOP_LEVEL.has(segments[0] ?? "") || SUPPORT_FOLDERS.has(segments[0] ?? "");
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Where each top-level key of the exported `locales` object lives: directly on
|
|
27
|
-
* the object ("top") or as a namespaced object ("nested").
|
|
28
|
-
*/
|
|
29
|
-
function localePlacement(text) {
|
|
30
|
-
const sourceFile = ts.createSourceFile("locales.ts", text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
31
|
-
for (const statement of sourceFile.statements) {
|
|
32
|
-
if (!ts.isVariableStatement(statement))
|
|
33
|
-
continue;
|
|
34
|
-
const isExported = (ts.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
35
|
-
if (!isExported)
|
|
36
|
-
continue;
|
|
37
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
38
|
-
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "locales")
|
|
39
|
-
continue;
|
|
40
|
-
if (!declaration.initializer || !ts.isObjectLiteralExpression(declaration.initializer))
|
|
41
|
-
continue;
|
|
42
|
-
const placement = new Map();
|
|
43
|
-
for (const property of declaration.initializer.properties) {
|
|
44
|
-
if (!ts.isPropertyAssignment(property))
|
|
45
|
-
continue;
|
|
46
|
-
let name;
|
|
47
|
-
if (ts.isIdentifier(property.name))
|
|
48
|
-
name = property.name.text;
|
|
49
|
-
else if (ts.isStringLiteral(property.name))
|
|
50
|
-
name = property.name.text;
|
|
51
|
-
if (name === undefined)
|
|
52
|
-
continue;
|
|
53
|
-
const line = sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1;
|
|
54
|
-
placement.set(name, {
|
|
55
|
-
kind: ts.isObjectLiteralExpression(property.initializer) ? "nested" : "top",
|
|
56
|
-
line,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return placement;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
export const localePlacementRule = {
|
|
65
|
-
meta: {
|
|
66
|
-
schema: [],
|
|
67
|
-
type: "problem",
|
|
68
|
-
docs: {
|
|
69
|
-
description: "Require shared locales at the top level and single-feature locales in a namespaced object.",
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
create(context) {
|
|
73
|
-
const sourceRoot = path.resolve("src");
|
|
74
|
-
const file = path.resolve(context.filename);
|
|
75
|
-
const segments = segmentsOf(file, sourceRoot);
|
|
76
|
-
if (segments.length === 0)
|
|
77
|
-
return {};
|
|
78
|
-
const index = getProjectIndex(sourceRoot);
|
|
79
|
-
if (!index)
|
|
80
|
-
return {};
|
|
81
|
-
// The named locales object lives in src/locales/index.ts; the rule reports
|
|
82
|
-
// on that file, because the fix is in the definition.
|
|
83
|
-
const localesFile = [...index.modules.keys()].find((candidate) => {
|
|
84
|
-
const candidateSegments = segmentsOf(candidate, sourceRoot);
|
|
85
|
-
return candidateSegments.length === 2 && candidateSegments[0] === "locales" && candidateSegments[1]?.startsWith("index.");
|
|
86
|
-
});
|
|
87
|
-
if (!localesFile || file !== localesFile)
|
|
88
|
-
return {};
|
|
89
|
-
const placement = localePlacement(readFileSync(localesFile, "utf8"));
|
|
90
|
-
if (!placement)
|
|
91
|
-
return {};
|
|
92
|
-
const keyReaders = new Map();
|
|
93
|
-
const keyFeatures = new Map();
|
|
94
|
-
const keyForcedTopLevel = new Set();
|
|
95
|
-
for (const [candidateFile, module] of index.modules) {
|
|
96
|
-
if (candidateFile === localesFile)
|
|
97
|
-
continue;
|
|
98
|
-
const importsLocales = module.imports.some((moduleImport) => resolveSpecifier(candidateFile, moduleImport.specifier, sourceRoot) === localesFile);
|
|
99
|
-
if (!importsLocales)
|
|
100
|
-
continue;
|
|
101
|
-
const candidateSegments = segmentsOf(candidateFile, sourceRoot);
|
|
102
|
-
for (const match of readFileSync(candidateFile, "utf8").matchAll(LOCALE_ACCESS)) {
|
|
103
|
-
const key = match.groups?.key;
|
|
104
|
-
if (key === undefined)
|
|
105
|
-
continue;
|
|
106
|
-
const readers = keyReaders.get(key) ?? new Set();
|
|
107
|
-
readers.add(candidateFile);
|
|
108
|
-
keyReaders.set(key, readers);
|
|
109
|
-
if (candidateSegments[0] === "features" && candidateSegments.length >= 2) {
|
|
110
|
-
const features = keyFeatures.get(key) ?? new Set();
|
|
111
|
-
features.add(candidateSegments[1] ?? "");
|
|
112
|
-
keyFeatures.set(key, features);
|
|
113
|
-
}
|
|
114
|
-
else if (forcesTopLevel(candidateSegments)) {
|
|
115
|
-
keyForcedTopLevel.add(key);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const findings = [];
|
|
120
|
-
for (const [key, readers] of keyReaders) {
|
|
121
|
-
const features = keyFeatures.get(key) ?? new Set();
|
|
122
|
-
const shared = features.size > 1 || keyForcedTopLevel.has(key);
|
|
123
|
-
const current = placement.get(key);
|
|
124
|
-
if (shared) {
|
|
125
|
-
if (current?.kind === "nested") {
|
|
126
|
-
findings.push({
|
|
127
|
-
line: current.line,
|
|
128
|
-
message: `Locale "${key}" is read by ${describeConsumers([...readers], sourceRoot)} and must live ` +
|
|
129
|
-
"at the top level of locales. See docs/code-organization-guide/rules/locales-rule.md",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (features.size !== 1)
|
|
135
|
-
continue;
|
|
136
|
-
const featureName = [...features][0] ?? "";
|
|
137
|
-
const expected = camelCase(featureName);
|
|
138
|
-
if (current?.kind === "top") {
|
|
139
|
-
findings.push({
|
|
140
|
-
line: current.line,
|
|
141
|
-
message: `Locale "${key}" is read only by the ${featureName} feature; it must live in an object ` +
|
|
142
|
-
`named "${expected}". See docs/code-organization-guide/rules/locales-rule.md`,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
else if (current?.kind === "nested" && key !== expected) {
|
|
146
|
-
findings.push({
|
|
147
|
-
line: current.line,
|
|
148
|
-
message: `Locale "${key}" is read only by the ${featureName} feature; it must live in an object ` +
|
|
149
|
-
`named "${expected}", not "${key}". See docs/code-organization-guide/rules/locales-rule.md`,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (findings.length === 0)
|
|
154
|
-
return {};
|
|
155
|
-
return {
|
|
156
|
-
Program(node) {
|
|
157
|
-
for (const finding of findings) {
|
|
158
|
-
context.report({ node, loc: { line: finding.line, column: 0 }, message: finding.message });
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
},
|
|
163
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/locales-location
|
|
3
|
-
*
|
|
4
|
-
* All locales MUST live in the named locales object exported from
|
|
5
|
-
* src/locales/index.ts. User-facing strings defined outside the locales
|
|
6
|
-
* directory are reported.
|
|
7
|
-
*
|
|
8
|
-
* @see docs/code-organization-guide/rules/locales-rule.md
|
|
9
|
-
*/
|
|
10
|
-
import type { Rule } from "eslint";
|
|
11
|
-
export declare const localesLocationRule: Rule.RuleModule;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: pasika/locales-location
|
|
3
|
-
*
|
|
4
|
-
* All locales MUST live in the named locales object exported from
|
|
5
|
-
* src/locales/index.ts. User-facing strings defined outside the locales
|
|
6
|
-
* directory are reported.
|
|
7
|
-
*
|
|
8
|
-
* @see docs/code-organization-guide/rules/locales-rule.md
|
|
9
|
-
*/
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
function isLocalesFile(filename) {
|
|
12
|
-
const segments = path.resolve(filename).split(path.sep);
|
|
13
|
-
const srcIdx = segments.lastIndexOf("src");
|
|
14
|
-
return srcIdx !== -1 && segments[srcIdx + 1] === "locales";
|
|
15
|
-
}
|
|
16
|
-
const LOCALE_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
17
|
-
function looksLikeLocaleKey(name) {
|
|
18
|
-
return LOCALE_NAME_RE.test(name);
|
|
19
|
-
}
|
|
20
|
-
export const localesLocationRule = {
|
|
21
|
-
meta: {
|
|
22
|
-
schema: [],
|
|
23
|
-
type: "problem",
|
|
24
|
-
docs: {
|
|
25
|
-
description: "Require all locales to live in the named locales object.",
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
create(context) {
|
|
29
|
-
if (isLocalesFile(context.filename))
|
|
30
|
-
return {};
|
|
31
|
-
const filename = context.filename;
|
|
32
|
-
const segments = path.resolve(filename).split(path.sep);
|
|
33
|
-
const srcIdx = segments.lastIndexOf("src");
|
|
34
|
-
if (srcIdx === -1)
|
|
35
|
-
return {};
|
|
36
|
-
const folder = segments[srcIdx + 1];
|
|
37
|
-
if (folder === "app" || folder === "config")
|
|
38
|
-
return {};
|
|
39
|
-
return {
|
|
40
|
-
VariableDeclarator(node) {
|
|
41
|
-
if (node.id.type === "Identifier" &&
|
|
42
|
-
node.init?.type === "ObjectExpression" &&
|
|
43
|
-
node.init.properties.length > 0 &&
|
|
44
|
-
looksLikeLocaleKey(node.id.name)) {
|
|
45
|
-
const hasStringValues = node.init.properties.some((p) => p.type === "Property" && p.value.type === "Literal");
|
|
46
|
-
if (hasStringValues) {
|
|
47
|
-
context.report({
|
|
48
|
-
node,
|
|
49
|
-
message: "User-facing strings must live in src/locales/, not inline in component files.",
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { getFilename } from "./helpers.js";
|
|
2
|
-
export const docKindSuffixRule = {
|
|
3
|
-
meta: {
|
|
4
|
-
type: "problem",
|
|
5
|
-
docs: {
|
|
6
|
-
description: "Documentation files must have a -guide, -rule, -reference, or -policy suffix.",
|
|
7
|
-
recommended: true,
|
|
8
|
-
},
|
|
9
|
-
},
|
|
10
|
-
create(context) {
|
|
11
|
-
return {
|
|
12
|
-
root(node) {
|
|
13
|
-
const filename = getFilename(context);
|
|
14
|
-
if (!filename.endsWith(".md"))
|
|
15
|
-
return;
|
|
16
|
-
if (filename.endsWith("-guide.md") ||
|
|
17
|
-
filename.endsWith("-rule.md") ||
|
|
18
|
-
filename.endsWith("-reference.md") ||
|
|
19
|
-
filename.endsWith("-policy.md")) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
context.report({
|
|
23
|
-
node,
|
|
24
|
-
message: "file name carries no -guide, -rule, -reference, or -policy suffix",
|
|
25
|
-
});
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
},
|
|
29
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,99 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
};
|