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.
Files changed (189) hide show
  1. package/README.md +72 -20
  2. package/dist/cli/index.d.ts +0 -1
  3. package/dist/cli/index.js +4999 -103
  4. package/dist/eslint/pasika/index.d.ts +165 -53
  5. package/dist/eslint/pasika/index.js +4424 -89
  6. package/package.json +5 -6
  7. package/dist/cli/output.d.ts +0 -3
  8. package/dist/cli/output.js +0 -11
  9. package/dist/enforcement/coverage.d.ts +0 -51
  10. package/dist/enforcement/coverage.js +0 -206
  11. package/dist/enforcement/doctor.d.ts +0 -16
  12. package/dist/enforcement/doctor.js +0 -249
  13. package/dist/enforcement/normalize.d.ts +0 -11
  14. package/dist/enforcement/normalize.js +0 -21
  15. package/dist/enforcement/parse-docs.d.ts +0 -58
  16. package/dist/enforcement/parse-docs.js +0 -94
  17. package/dist/enforcement/types.d.ts +0 -54
  18. package/dist/enforcement/types.js +0 -50
  19. package/dist/eslint/pasika/ast-types.d.ts +0 -122
  20. package/dist/eslint/pasika/ast-types.js +0 -1
  21. package/dist/eslint/pasika/project/ccf.d.ts +0 -48
  22. package/dist/eslint/pasika/project/ccf.js +0 -119
  23. package/dist/eslint/pasika/project/index.d.ts +0 -21
  24. package/dist/eslint/pasika/project/index.js +0 -139
  25. package/dist/eslint/pasika/project/parse-module.d.ts +0 -27
  26. package/dist/eslint/pasika/project/parse-module.js +0 -128
  27. package/dist/eslint/pasika/rules/application-structure.d.ts +0 -2
  28. package/dist/eslint/pasika/rules/application-structure.js +0 -152
  29. package/dist/eslint/pasika/rules/component-conventions.d.ts +0 -16
  30. package/dist/eslint/pasika/rules/component-conventions.js +0 -145
  31. package/dist/eslint/pasika/rules/component-nesting.d.ts +0 -12
  32. package/dist/eslint/pasika/rules/component-nesting.js +0 -69
  33. package/dist/eslint/pasika/rules/component-placement.d.ts +0 -11
  34. package/dist/eslint/pasika/rules/component-placement.js +0 -75
  35. package/dist/eslint/pasika/rules/config-extraction.d.ts +0 -14
  36. package/dist/eslint/pasika/rules/config-extraction.js +0 -74
  37. package/dist/eslint/pasika/rules/cross-feature-import.d.ts +0 -2
  38. package/dist/eslint/pasika/rules/cross-feature-import.js +0 -75
  39. package/dist/eslint/pasika/rules/css/apply-usage.d.ts +0 -10
  40. package/dist/eslint/pasika/rules/css/apply-usage.js +0 -45
  41. package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +0 -10
  42. package/dist/eslint/pasika/rules/css/base-layer-pair.js +0 -49
  43. package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +0 -10
  44. package/dist/eslint/pasika/rules/css/css-variable-naming.js +0 -39
  45. package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +0 -11
  46. package/dist/eslint/pasika/rules/css/custom-utility-apply.js +0 -52
  47. package/dist/eslint/pasika/rules/css/global-css-location.d.ts +0 -10
  48. package/dist/eslint/pasika/rules/css/global-css-location.js +0 -42
  49. package/dist/eslint/pasika/rules/css/helpers.d.ts +0 -29
  50. package/dist/eslint/pasika/rules/css/helpers.js +0 -93
  51. package/dist/eslint/pasika/rules/css/index.d.ts +0 -13
  52. package/dist/eslint/pasika/rules/css/index.js +0 -25
  53. package/dist/eslint/pasika/rules/css/root-variables.d.ts +0 -11
  54. package/dist/eslint/pasika/rules/css/root-variables.js +0 -53
  55. package/dist/eslint/pasika/rules/css/rule-tester.d.ts +0 -11
  56. package/dist/eslint/pasika/rules/css/rule-tester.js +0 -22
  57. package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +0 -11
  58. package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +0 -76
  59. package/dist/eslint/pasika/rules/css/surface-utility.d.ts +0 -10
  60. package/dist/eslint/pasika/rules/css/surface-utility.js +0 -41
  61. package/dist/eslint/pasika/rules/css/theme-reset.d.ts +0 -9
  62. package/dist/eslint/pasika/rules/css/theme-reset.js +0 -36
  63. package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +0 -11
  64. package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +0 -53
  65. package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +0 -10
  66. package/dist/eslint/pasika/rules/cva-appearance-props.js +0 -77
  67. package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +0 -11
  68. package/dist/eslint/pasika/rules/cva-boolean-variants.js +0 -97
  69. package/dist/eslint/pasika/rules/data-testid-case.d.ts +0 -2
  70. package/dist/eslint/pasika/rules/data-testid-case.js +0 -76
  71. package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +0 -9
  72. package/dist/eslint/pasika/rules/enforce-barrel-exports.js +0 -78
  73. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +0 -21
  74. package/dist/eslint/pasika/rules/enforce-cn-merge.js +0 -86
  75. package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +0 -9
  76. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +0 -86
  77. package/dist/eslint/pasika/rules/filename-case.d.ts +0 -2
  78. package/dist/eslint/pasika/rules/filename-case.js +0 -123
  79. package/dist/eslint/pasika/rules/hook-complexity.d.ts +0 -12
  80. package/dist/eslint/pasika/rules/hook-complexity.js +0 -114
  81. package/dist/eslint/pasika/rules/hook-extraction.d.ts +0 -11
  82. package/dist/eslint/pasika/rules/hook-extraction.js +0 -55
  83. package/dist/eslint/pasika/rules/import-boundaries.d.ts +0 -2
  84. package/dist/eslint/pasika/rules/import-boundaries.js +0 -167
  85. package/dist/eslint/pasika/rules/import-through-index.d.ts +0 -2
  86. package/dist/eslint/pasika/rules/import-through-index.js +0 -52
  87. package/dist/eslint/pasika/rules/interactive-component.d.ts +0 -10
  88. package/dist/eslint/pasika/rules/interactive-component.js +0 -76
  89. package/dist/eslint/pasika/rules/json/index.d.ts +0 -5
  90. package/dist/eslint/pasika/rules/json/index.js +0 -9
  91. package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +0 -10
  92. package/dist/eslint/pasika/rules/json/no-cache-flag.js +0 -45
  93. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +0 -10
  94. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +0 -41
  95. package/dist/eslint/pasika/rules/json/rule-tester.d.ts +0 -11
  96. package/dist/eslint/pasika/rules/json/rule-tester.js +0 -21
  97. package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +0 -9
  98. package/dist/eslint/pasika/rules/jsx-hygiene.js +0 -108
  99. package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +0 -10
  100. package/dist/eslint/pasika/rules/locale-dotted-path.js +0 -65
  101. package/dist/eslint/pasika/rules/locale-placement.d.ts +0 -13
  102. package/dist/eslint/pasika/rules/locale-placement.js +0 -163
  103. package/dist/eslint/pasika/rules/locales-location.d.ts +0 -11
  104. package/dist/eslint/pasika/rules/locales-location.js +0 -56
  105. package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +0 -5
  106. package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +0 -29
  107. package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +0 -5
  108. package/dist/eslint/pasika/rules/md/example-heading-description.js +0 -30
  109. package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +0 -2
  110. package/dist/eslint/pasika/rules/md/glossary-term-linking.js +0 -99
  111. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +0 -2
  112. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +0 -46
  113. package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +0 -5
  114. package/dist/eslint/pasika/rules/md/guide-link-anchors.js +0 -50
  115. package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +0 -5
  116. package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +0 -51
  117. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +0 -5
  118. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +0 -26
  119. package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +0 -5
  120. package/dist/eslint/pasika/rules/md/guide-step-single-link.js +0 -48
  121. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +0 -5
  122. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +0 -42
  123. package/dist/eslint/pasika/rules/md/helpers.d.ts +0 -25
  124. package/dist/eslint/pasika/rules/md/helpers.js +0 -37
  125. package/dist/eslint/pasika/rules/md/index.d.ts +0 -27
  126. package/dist/eslint/pasika/rules/md/index.js +0 -53
  127. package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +0 -5
  128. package/dist/eslint/pasika/rules/md/no-cross-document-link.js +0 -36
  129. package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +0 -5
  130. package/dist/eslint/pasika/rules/md/no-nested-how-to.js +0 -39
  131. package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +0 -5
  132. package/dist/eslint/pasika/rules/md/no-template-prompt.js +0 -22
  133. package/dist/eslint/pasika/rules/md/overview-length.d.ts +0 -5
  134. package/dist/eslint/pasika/rules/md/overview-length.js +0 -47
  135. package/dist/eslint/pasika/rules/md/overview-present.d.ts +0 -5
  136. package/dist/eslint/pasika/rules/md/overview-present.js +0 -41
  137. package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +0 -5
  138. package/dist/eslint/pasika/rules/md/policy-no-examples.js +0 -28
  139. package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +0 -5
  140. package/dist/eslint/pasika/rules/md/policy-single-document.js +0 -34
  141. package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +0 -5
  142. package/dist/eslint/pasika/rules/md/policy-subject-headings.js +0 -58
  143. package/dist/eslint/pasika/rules/md/project-index.d.ts +0 -17
  144. package/dist/eslint/pasika/rules/md/project-index.js +0 -67
  145. package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +0 -6
  146. package/dist/eslint/pasika/rules/md/reference-block-headings.js +0 -31
  147. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +0 -6
  148. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +0 -26
  149. package/dist/eslint/pasika/rules/md/requirement-present.d.ts +0 -5
  150. package/dist/eslint/pasika/rules/md/requirement-present.js +0 -41
  151. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +0 -5
  152. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +0 -46
  153. package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +0 -5
  154. package/dist/eslint/pasika/rules/md/rule-paired-examples.js +0 -36
  155. package/dist/eslint/pasika/rules/md/rule-tester.d.ts +0 -11
  156. package/dist/eslint/pasika/rules/md/rule-tester.js +0 -21
  157. package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +0 -2
  158. package/dist/eslint/pasika/rules/md/support-document-placement.js +0 -36
  159. package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +0 -2
  160. package/dist/eslint/pasika/rules/md/title-matches-file-name.js +0 -46
  161. package/dist/eslint/pasika/rules/named-exports.d.ts +0 -2
  162. package/dist/eslint/pasika/rules/named-exports.js +0 -49
  163. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +0 -9
  164. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +0 -97
  165. package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +0 -10
  166. package/dist/eslint/pasika/rules/no-eslint-disable.js +0 -33
  167. package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +0 -9
  168. package/dist/eslint/pasika/rules/no-mixed-concerns.js +0 -71
  169. package/dist/eslint/pasika/rules/no-util-barrel.d.ts +0 -2
  170. package/dist/eslint/pasika/rules/no-util-barrel.js +0 -48
  171. package/dist/eslint/pasika/rules/pure-function-extract.d.ts +0 -9
  172. package/dist/eslint/pasika/rules/pure-function-extract.js +0 -92
  173. package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +0 -13
  174. package/dist/eslint/pasika/rules/shared-style-dedup.js +0 -91
  175. package/dist/eslint/pasika/rules/stay-flat.d.ts +0 -12
  176. package/dist/eslint/pasika/rules/stay-flat.js +0 -81
  177. package/dist/eslint/pasika/rules/support-file-placement.d.ts +0 -15
  178. package/dist/eslint/pasika/rules/support-file-placement.js +0 -70
  179. package/dist/eslint/pasika/rules/support-folder-shape.d.ts +0 -2
  180. package/dist/eslint/pasika/rules/support-folder-shape.js +0 -59
  181. package/dist/eslint/pasika/rules/type-extraction.d.ts +0 -13
  182. package/dist/eslint/pasika/rules/type-extraction.js +0 -85
  183. package/dist/eslint/pasika/rules/ui-state.d.ts +0 -9
  184. package/dist/eslint/pasika/rules/ui-state.js +0 -99
  185. package/dist/eslint/pasika/rules/util-file-name.d.ts +0 -2
  186. package/dist/eslint/pasika/rules/util-file-name.js +0 -48
  187. package/dist/eslint/pasika/rules/value-extraction.d.ts +0 -13
  188. package/dist/eslint/pasika/rules/value-extraction.js +0 -48
  189. package/enforcement/registry.json +0 -1145
@@ -1,94 +0,0 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
- import path from "node:path";
3
- import { hashRequirement, normalizeRequirement } from "./normalize.js";
4
- const KIND_BY_SUFFIX = [
5
- ["-rule.md", "rule"],
6
- ["-guide.md", "guide"],
7
- ["-reference.md", "reference"],
8
- ["-policy.md", "policy"],
9
- ];
10
- /** Words that carry requirement strength, longest first so `MUST NOT` wins over `MUST`. */
11
- const RFC_2119 = /\b(?<keyword>MUST NOT|MUST|SHOULD NOT|SHOULD|MAY)\b/;
12
- function listMarkdownFiles(dir) {
13
- return readdirSync(dir).flatMap((entry) => {
14
- const entryPath = path.join(dir, entry);
15
- if (statSync(entryPath).isDirectory()) {
16
- // Underscore folders hold templates and other support assets, not documents.
17
- return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
18
- }
19
- return entry.endsWith(".md") ? [entryPath] : [];
20
- });
21
- }
22
- /**
23
- * Blanks fenced code blocks and unwraps inline code spans, keeping one entry per
24
- * source line so reported line numbers still match the file.
25
- */
26
- function toProse(body, codeSpanReplacement) {
27
- let insideFence = false;
28
- return body.split("\n").map((line) => {
29
- if (/^\s*(?:```|~~~)/.test(line)) {
30
- insideFence = !insideFence;
31
- return "";
32
- }
33
- if (insideFence)
34
- return "";
35
- return line.replaceAll(/`(?<content>[^`]*)`/g, codeSpanReplacement);
36
- });
37
- }
38
- function findDocLinks(line) {
39
- return [...line.matchAll(/\]\((?<target>[^)]*\.md[^)]*)\)/g)].map((match) => match.groups?.target ?? "");
40
- }
41
- export function parseDoc(filePath, docsRoot) {
42
- const body = readFileSync(filePath, "utf8");
43
- const prose = toProse(body, "$<content>");
44
- const proseWithoutCode = toProse(body, "");
45
- const fileName = path.basename(filePath);
46
- const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
47
- const requirements = [];
48
- const steps = [];
49
- const exampleHeadings = [];
50
- const sectionHeadings = [];
51
- const docLinks = [];
52
- prose.forEach((line, index) => {
53
- const lineNumber = index + 1;
54
- for (const target of findDocLinks(line)) {
55
- docLinks.push({ line: lineNumber, target });
56
- }
57
- if (/^\s*[-*]\s/.test(line) && RFC_2119.test(line)) {
58
- const text = normalizeRequirement(line);
59
- requirements.push({ text, hash: hashRequirement(text), raw: line.replace(/^\s*[-*]\s+/, ""), line: lineNumber });
60
- }
61
- if (/^\s*\d+\.\s/.test(line)) {
62
- steps.push({ line: lineNumber, text: line.replace(/^\s*\d+\.\s+/, ""), links: findDocLinks(line) });
63
- }
64
- if (/^## (?:Incorrect|Correct)\b/.test(line)) {
65
- exampleHeadings.push({ line: lineNumber, text: line.slice(3).trim() });
66
- }
67
- else if (line.startsWith("## ")) {
68
- sectionHeadings.push({ line: lineNumber, text: line.slice(3).trim() });
69
- }
70
- });
71
- const title = /^# (?<title>.+)$/m.exec(body)?.groups?.title?.trim() ?? "";
72
- const overview = prose.slice(1).find((line) => line.trim() !== "" && !line.startsWith("#"));
73
- return {
74
- filePath,
75
- doc: path.relative(docsRoot, filePath).split(path.sep).join("/"),
76
- fileName,
77
- kind,
78
- title,
79
- overview,
80
- prose,
81
- proseWithoutCode,
82
- requirements,
83
- steps,
84
- exampleHeadings,
85
- docLinks,
86
- sectionHeadings,
87
- };
88
- }
89
- export function parseDocs(docsRoot) {
90
- return listMarkdownFiles(docsRoot)
91
- .sort((a, b) => a.localeCompare(b))
92
- .map((filePath) => parseDoc(filePath, docsRoot));
93
- }
94
- export { RFC_2119 };
@@ -1,54 +0,0 @@
1
- /**
2
- * The enforcement registry — the record of how every documented requirement is
3
- * checked.
4
- *
5
- * A requirement is identified by the hash of its canonical text, not by a
6
- * hand-written id. Rewording a requirement therefore changes its hash, which
7
- * makes `pasika coverage` report it as changed until someone re-confirms that
8
- * the recorded enforcement still covers it.
9
- */
10
- import { z } from "zod";
11
- export declare const enforcementKindSchema: z.ZodEnum<{
12
- eslint: "eslint";
13
- doctor: "doctor";
14
- judgment: "judgment";
15
- permission: "permission";
16
- planned: "planned";
17
- }>;
18
- export declare const requirementSchema: z.ZodObject<{
19
- doc: z.ZodString;
20
- text: z.ZodString;
21
- hash: z.ZodString;
22
- kind: z.ZodEnum<{
23
- eslint: "eslint";
24
- doctor: "doctor";
25
- judgment: "judgment";
26
- permission: "permission";
27
- planned: "planned";
28
- }>;
29
- ref: z.ZodOptional<z.ZodString>;
30
- note: z.ZodOptional<z.ZodString>;
31
- }, z.core.$strip>;
32
- export declare const registrySchema: z.ZodObject<{
33
- requirements: z.ZodArray<z.ZodObject<{
34
- doc: z.ZodString;
35
- text: z.ZodString;
36
- hash: z.ZodString;
37
- kind: z.ZodEnum<{
38
- eslint: "eslint";
39
- doctor: "doctor";
40
- judgment: "judgment";
41
- permission: "permission";
42
- planned: "planned";
43
- }>;
44
- ref: z.ZodOptional<z.ZodString>;
45
- note: z.ZodOptional<z.ZodString>;
46
- }, z.core.$strip>>;
47
- }, z.core.$strip>;
48
- export type EnforcementKind = z.infer<typeof enforcementKindSchema>;
49
- export type Requirement = z.infer<typeof requirementSchema>;
50
- export type Registry = z.infer<typeof registrySchema>;
51
- /** Every kind, in the order the coverage summary prints them. */
52
- export declare const ENFORCEMENT_KINDS: EnforcementKind[];
53
- /** The kinds that count as mechanically enforced today. */
54
- export declare const MECHANICAL_KINDS: EnforcementKind[];
@@ -1,50 +0,0 @@
1
- /**
2
- * The enforcement registry — the record of how every documented requirement is
3
- * checked.
4
- *
5
- * A requirement is identified by the hash of its canonical text, not by a
6
- * hand-written id. Rewording a requirement therefore changes its hash, which
7
- * makes `pasika coverage` report it as changed until someone re-confirms that
8
- * the recorded enforcement still covers it.
9
- */
10
- import { z } from "zod";
11
- export const enforcementKindSchema = z.enum([
12
- /** An ESLint rule reports it. */
13
- "eslint",
14
- /** A `pasika doctor` check reports it. */
15
- "doctor",
16
- /** No mechanical check can decide it; a reviewer or agent applies it. */
17
- "judgment",
18
- /** The requirement grants permission, so there is nothing to check. */
19
- "permission",
20
- /** A mechanical check is possible but not written yet. */
21
- "planned",
22
- ]);
23
- export const requirementSchema = z.object({
24
- /** Document path relative to the docs root. */
25
- doc: z.string(),
26
- /** Canonical requirement text: links flattened, code spans unwrapped, whitespace collapsed. */
27
- text: z.string(),
28
- /** Short hash of `text`. */
29
- hash: z.string(),
30
- kind: enforcementKindSchema,
31
- /**
32
- * Identifier of the check that covers this requirement: an ESLint rule id or
33
- * a doctor check id. Several ids are comma-separated.
34
- * Absent for judgment and permission.
35
- */
36
- ref: z.string().optional(),
37
- /**
38
- * For `judgment`, why no mechanical check can decide it. For `planned`, the
39
- * check that should cover it. For `eslint` and `doctor`, what the existing
40
- * check does not cover.
41
- */
42
- note: z.string().optional(),
43
- });
44
- export const registrySchema = z.object({
45
- requirements: z.array(requirementSchema),
46
- });
47
- /** Every kind, in the order the coverage summary prints them. */
48
- export const ENFORCEMENT_KINDS = ["eslint", "doctor", "planned", "judgment", "permission"];
49
- /** The kinds that count as mechanically enforced today. */
50
- export const MECHANICAL_KINDS = ["eslint", "doctor"];
@@ -1,122 +0,0 @@
1
- /**
2
- * Local AST node types for the parser-specific shapes ESLint's ESTree types
3
- * do not carry: JSX nodes and TypeScript type nodes.
4
- *
5
- * Every extra field is optional so that any `Rule.Node` is assignable to these
6
- * types — the handler parameters of the rules must accept whatever the parser
7
- * hands them. Narrowing happens with `type`-discriminant checks inside each
8
- * rule, exactly as in `no-arbitrary-tailwind.ts`.
9
- */
10
- import type { Rule } from "eslint";
11
- import type * as ESTree from "estree";
12
- /** JSXIdentifier — the parser-specific name node of a JSX element or attribute. */
13
- export interface JsxIdentifier {
14
- type: "JSXIdentifier";
15
- name: string;
16
- }
17
- /** JSXMemberExpression — `<Foo.Bar>`, used by element and attribute names. */
18
- export interface JsxMemberExpression {
19
- type: "JSXMemberExpression";
20
- object: JsxIdentifier | JsxMemberExpression;
21
- property: JsxIdentifier;
22
- }
23
- /** JSXExpressionContainer — the `{expression}` part of a JSX attribute value. */
24
- export interface JsxExpressionContainer {
25
- type: "JSXExpressionContainer";
26
- expression: ESTree.Expression;
27
- }
28
- /** JSXAttribute — `name="value"` or `name={expression}`. */
29
- export type JsxAttribute = Omit<Rule.Node, "type"> & {
30
- type: string;
31
- name: JsxIdentifier;
32
- value: ESTree.Literal | JsxExpressionContainer | null;
33
- parent?: JsxOpeningElementNode;
34
- };
35
- /** JSXSpreadAttribute — `{...props}`. */
36
- export interface JsxSpreadAttribute {
37
- type: "JSXSpreadAttribute";
38
- name?: never;
39
- value?: never;
40
- argument: ESTree.Expression;
41
- }
42
- /** Anything that can appear inside `attributes` of a JSX opening element. */
43
- export type JsxAttributeLike = JsxAttribute | JsxSpreadAttribute;
44
- /** JSXOpeningElement — the part before the closing `>` of an element. */
45
- export interface JsxOpeningElement {
46
- type: "JSXOpeningElement";
47
- name: JsxIdentifier | JsxMemberExpression;
48
- attributes: JsxAttributeLike[];
49
- }
50
- /** A node that may be a JSX element — used for parents and helper parameters. */
51
- export type MaybeJsxElement = Rule.Node & {
52
- openingElement?: JsxOpeningElement;
53
- };
54
- /** JSXElement as received by a `JSXElement` visitor. */
55
- export type JsxElementNode = MaybeJsxElement & {
56
- parent?: MaybeJsxElement;
57
- };
58
- /** JSXAttribute as received by a `JSXAttribute` visitor, with its parent chain. */
59
- export type JsxAttributeNode = Rule.Node & {
60
- name?: JsxIdentifier;
61
- value?: ESTree.Literal | JsxExpressionContainer | null;
62
- parent?: JsxOpeningElementNode;
63
- };
64
- /** JSXOpeningElement reached through a parent chain. */
65
- export type JsxOpeningElementNode = Rule.Node & {
66
- name?: JsxIdentifier;
67
- attributes?: JsxAttributeLike[];
68
- parent?: JsxElementNode;
69
- };
70
- /**
71
- * FunctionDeclaration as seen by a rule visitor. ESLint's ESTree typing says
72
- * `id` is always present, but an anonymous `export default function () {}`
73
- * reaches the visitor with `id: null`, so the field is kept nullable.
74
- */
75
- export type FunctionDeclarationNode = Rule.Node & {
76
- id?: {
77
- name?: string;
78
- } | null;
79
- body?: ESTree.BlockStatement;
80
- };
81
- /**
82
- * TSTypeAliasDeclaration — `type X = ...`, from the typescript-eslint parser.
83
- *
84
- * Kept as a plain `Rule.Node` intersection so it can be passed to
85
- * `context.report`, which requires an ESTree node.
86
- */
87
- export type TsTypeAliasDeclarationNode = Rule.Node & {
88
- id?: {
89
- type?: string;
90
- name?: string;
91
- };
92
- typeAnnotation?: TsTypeNode;
93
- };
94
- /**
95
- * A member of a type literal (TSPropertySignature, TSIndexSignature, ...).
96
- *
97
- * The `type` field is widened (via Omit) so it can be compared against the
98
- * parser-specific "TSPropertySignature" string that ESTree's union lacks.
99
- */
100
- export type TsTypeElementNode = Omit<Rule.Node, "type"> & {
101
- type: string;
102
- key?: {
103
- type?: string;
104
- name?: string;
105
- };
106
- typeAnnotation?: TsTypeAnnotationNode;
107
- };
108
- /** TSTypeAnnotation — the `: T` wrapper around a type node. */
109
- export type TsTypeAnnotationNode = Omit<Rule.Node, "type"> & {
110
- type: string;
111
- typeAnnotation?: TsTypeNode;
112
- };
113
- /** Any TypeScript type node the rules read (TSTypeLiteral, TSUnionType, ...). */
114
- export type TsTypeNode = Omit<Rule.Node, "type"> & {
115
- type: string;
116
- members?: TsTypeElementNode[];
117
- types?: TsTypeNode[];
118
- literal?: {
119
- type?: string;
120
- value?: unknown;
121
- };
122
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,48 +0,0 @@
1
- import type { ProjectIndex } from "./index.js";
2
- export declare const SUPPORT_FOLDERS: Set<string>;
3
- /** Path of a file relative to the source root, as segments. */
4
- export declare function segmentsOf(file: string, sourceRoot: string): string[];
5
- /** Folder of a file relative to the source root, as segments. */
6
- export declare function folderSegmentsOf(file: string, sourceRoot: string): string[];
7
- export declare const isUnderApp: (segments: string[]) => boolean;
8
- export declare const isConfigModule: (segments: string[]) => boolean;
9
- export declare const isUnderCompositions: (segments: string[]) => boolean;
10
- export interface ComponentPlacement {
11
- /** Consumers that count toward the calculation, as absolute paths. */
12
- countedConsumers: string[];
13
- /** Folder the component belongs in, relative to the source root. */
14
- expectedFolder: string[];
15
- /** Why the expected folder is what it is, for the report. */
16
- reason: "ccf" | "across-features" | "across-layers";
17
- }
18
- /**
19
- * Resolves where a component belongs from the files that import it.
20
- *
21
- * Imports from `src/app/` and from configuration modules drop out, consumers
22
- * under `src/compositions/` count only when every consumer is there, and a
23
- * result of `src/features/` becomes `src/shared/` because no feature may import
24
- * from another. Returns undefined when no consumer counts, which is the case the
25
- * "lives in the feature it represents" requirement covers instead.
26
- */
27
- export declare function resolveComponentPlacement(componentFile: string, index: ProjectIndex): ComponentPlacement | undefined;
28
- export declare const formatFolder: (folder: string[]) => string;
29
- export interface SupportPlacement {
30
- countedConsumers: string[];
31
- expectedFolder: string[];
32
- reason: "app-consumer" | "config-module" | "ccf" | "across-features" | "across-layers";
33
- }
34
- /**
35
- * Resolves where a support file belongs from the files that import it.
36
- *
37
- * A consumer inside a support folder is owned by that folder's parent, so the
38
- * calculation lands on the scope that uses the file rather than on a sibling
39
- * support folder. A consumer under `src/app/` forces the root support folder, a
40
- * set of consumers inside one configuration module keeps the file in that module,
41
- * and consumers spanning features land in the root support folder.
42
- */
43
- export declare function resolveSupportPlacement(supportFile: string, supportFolder: string, index: ProjectIndex): SupportPlacement | undefined;
44
- /**
45
- * Consumers for a message. A widely used file can have dozens, so the list is
46
- * capped: the point is to show where the requirement comes from, not to enumerate.
47
- */
48
- export declare function describeConsumers(consumers: string[], sourceRoot: string): string;
@@ -1,119 +0,0 @@
1
- import path from "node:path";
2
- export const SUPPORT_FOLDERS = new Set(["hooks", "types", "schemas", "constants", "utils"]);
3
- /** Path of a file relative to the source root, as segments. */
4
- export function segmentsOf(file, sourceRoot) {
5
- const relative = path.relative(sourceRoot, file);
6
- return relative.startsWith("..") ? [] : relative.split(path.sep);
7
- }
8
- /** Folder of a file relative to the source root, as segments. */
9
- export function folderSegmentsOf(file, sourceRoot) {
10
- return segmentsOf(file, sourceRoot).slice(0, -1);
11
- }
12
- export const isUnderApp = (segments) => segments[0] === "app";
13
- export const isConfigModule = (segments) => segments[0] === "config";
14
- export const isUnderCompositions = (segments) => segments[0] === "compositions";
15
- /** The longest folder prefix every path shares. */
16
- function commonPrefix(folders) {
17
- if (folders.length === 0)
18
- return [];
19
- const [first = []] = folders;
20
- const shared = [];
21
- for (const [depth, segment] of first.entries()) {
22
- if (!folders.every((folder) => folder[depth] === segment))
23
- break;
24
- shared.push(segment);
25
- }
26
- return shared;
27
- }
28
- /** Walks out of any trailing support folders, since a component never lives in one. */
29
- function outOfSupportFolders(folder) {
30
- const result = [...folder];
31
- while (result.length > 0 && SUPPORT_FOLDERS.has(result[result.length - 1] ?? ""))
32
- result.pop();
33
- return result;
34
- }
35
- /**
36
- * Resolves where a component belongs from the files that import it.
37
- *
38
- * Imports from `src/app/` and from configuration modules drop out, consumers
39
- * under `src/compositions/` count only when every consumer is there, and a
40
- * result of `src/features/` becomes `src/shared/` because no feature may import
41
- * from another. Returns undefined when no consumer counts, which is the case the
42
- * "lives in the feature it represents" requirement covers instead.
43
- */
44
- export function resolveComponentPlacement(componentFile, index) {
45
- const consumers = [...(index.consumers.get(componentFile) ?? [])];
46
- const relevant = consumers.filter((consumer) => {
47
- const segments = segmentsOf(consumer, index.sourceRoot);
48
- return segments.length > 0 && !isUnderApp(segments) && !isConfigModule(segments);
49
- });
50
- if (relevant.length === 0)
51
- return undefined;
52
- const outsideCompositions = relevant.filter((consumer) => !isUnderCompositions(segmentsOf(consumer, index.sourceRoot)));
53
- const counted = outsideCompositions.length > 0 ? outsideCompositions : relevant;
54
- const shared = outOfSupportFolders(commonPrefix(counted.map((consumer) => folderSegmentsOf(consumer, index.sourceRoot))));
55
- // A shared folder of `features` or of the source root itself means no single
56
- // feature owns the component, so it belongs to the shared layer.
57
- if (shared.length === 1 && shared[0] === "features") {
58
- return { countedConsumers: counted, expectedFolder: ["shared"], reason: "across-features" };
59
- }
60
- if (shared.length === 0) {
61
- return { countedConsumers: counted, expectedFolder: ["shared"], reason: "across-layers" };
62
- }
63
- return { countedConsumers: counted, expectedFolder: shared, reason: "ccf" };
64
- }
65
- export const formatFolder = (folder) => `src/${folder.join("/")}/`;
66
- /** The folder that owns a consumer: its own folder, stepped out of any support folder. */
67
- function owningFolderOf(consumer, sourceRoot) {
68
- return outOfSupportFolders(folderSegmentsOf(consumer, sourceRoot));
69
- }
70
- /** The configuration module a file belongs to. `config/<name>/...` only: `config/<file>.ts` is not a module. */
71
- const configModuleOf = (segments) => isConfigModule(segments) && segments.length >= 3 ? segments[1] : undefined;
72
- /**
73
- * Resolves where a support file belongs from the files that import it.
74
- *
75
- * A consumer inside a support folder is owned by that folder's parent, so the
76
- * calculation lands on the scope that uses the file rather than on a sibling
77
- * support folder. A consumer under `src/app/` forces the root support folder, a
78
- * set of consumers inside one configuration module keeps the file in that module,
79
- * and consumers spanning features land in the root support folder.
80
- */
81
- export function resolveSupportPlacement(supportFile, supportFolder, index) {
82
- const consumers = [...(index.consumers.get(supportFile) ?? [])].filter((consumer) => segmentsOf(consumer, index.sourceRoot).length > 0);
83
- if (consumers.length === 0)
84
- return undefined;
85
- const consumerSegments = consumers.map((consumer) => segmentsOf(consumer, index.sourceRoot));
86
- if (consumerSegments.some((segments) => isUnderApp(segments))) {
87
- return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "app-consumer" };
88
- }
89
- const configModules = new Set(consumerSegments.map((segments) => configModuleOf(segments)));
90
- const [onlyConfigModule] = [...configModules];
91
- if (configModules.size === 1 && onlyConfigModule !== undefined) {
92
- return {
93
- countedConsumers: consumers,
94
- expectedFolder: ["config", onlyConfigModule, supportFolder],
95
- reason: "config-module",
96
- };
97
- }
98
- const shared = commonPrefix(consumers.map((consumer) => owningFolderOf(consumer, index.sourceRoot)));
99
- if (shared.length === 1 && shared[0] === "features") {
100
- return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "across-features" };
101
- }
102
- if (shared.length === 0) {
103
- return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "across-layers" };
104
- }
105
- return { countedConsumers: consumers, expectedFolder: [...shared, supportFolder], reason: "ccf" };
106
- }
107
- /**
108
- * Consumers for a message. A widely used file can have dozens, so the list is
109
- * capped: the point is to show where the requirement comes from, not to enumerate.
110
- */
111
- export function describeConsumers(consumers, sourceRoot) {
112
- const shown = 3;
113
- const names = consumers
114
- .map((consumer) => path.relative(path.dirname(sourceRoot), consumer).split(path.sep).join("/"))
115
- .sort((left, right) => left.localeCompare(right));
116
- if (names.length <= shown)
117
- return names.join(", ");
118
- return `${names.slice(0, shown).join(", ")} and ${String(names.length - shown)} more`;
119
- }
@@ -1,21 +0,0 @@
1
- import { type ParsedModule } from "./parse-module.js";
2
- export interface ProjectIndex {
3
- sourceRoot: string;
4
- /** Absolute file path to its parsed module. */
5
- modules: Map<string, ParsedModule>;
6
- /** Absolute file path to the files that import it. */
7
- consumers: Map<string, Set<string>>;
8
- /** `file` and name joined by a NUL, to the files that import that name from it. */
9
- symbolConsumers: Map<string, Set<string>>;
10
- }
11
- export declare const symbolKey: (file: string, name: string) => string;
12
- /** Resolves an import specifier to a file inside the source tree, if it points at one. */
13
- export declare function resolveSpecifier(fromFile: string, specifier: string, sourceRoot: string): string | undefined;
14
- /**
15
- * The project index for a source tree, rebuilt only when the tree changed.
16
- * Returns undefined when the tree does not exist, which is how a repository
17
- * without a `src/` folder opts out of every cross-file rule.
18
- */
19
- export declare function getProjectIndex(sourceRoot: string): ProjectIndex | undefined;
20
- /** Drops the memoized index. Used by tests that write a fresh tree per case. */
21
- export declare function clearProjectIndex(): void;
@@ -1,139 +0,0 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import path from "node:path";
3
- import { parseModule } from "./parse-module.js";
4
- const MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5
- const INDEX_BASENAMES = ["index.ts", "index.tsx", "index.mts", "index.cts", "index.js", "index.jsx"];
6
- /**
7
- * How long a built index is trusted before its inputs are re-checked. A long
8
- * lived ESLint server keeps this module in memory across edits, so the index has
9
- * to notice a changed tree without re-reading it on every single file.
10
- */
11
- const REVALIDATE_AFTER_MS = 2000;
12
- export const symbolKey = (file, name) => `${file}\u0000${name}`;
13
- function listSourceFiles(dir) {
14
- let entries;
15
- try {
16
- entries = readdirSync(dir);
17
- }
18
- catch {
19
- return [];
20
- }
21
- return entries.flatMap((entry) => {
22
- if (entry.startsWith(".") || entry === "node_modules")
23
- return [];
24
- const entryPath = path.join(dir, entry);
25
- let stats;
26
- try {
27
- stats = statSync(entryPath);
28
- }
29
- catch {
30
- return [];
31
- }
32
- if (stats.isDirectory())
33
- return listSourceFiles(entryPath);
34
- return MODULE_EXTENSIONS.includes(path.extname(entry)) ? [entryPath] : [];
35
- });
36
- }
37
- /** Cheap fingerprint of the tree, so an unchanged tree is never re-parsed. */
38
- function fingerprint(files) {
39
- let total = 0;
40
- for (const file of files) {
41
- try {
42
- total += statSync(file).mtimeMs;
43
- }
44
- catch {
45
- /* A file removed between listing and stat just drops out of the fingerprint. */
46
- }
47
- }
48
- return `${String(files.length)}:${String(total)}`;
49
- }
50
- /** Resolves an import specifier to a file inside the source tree, if it points at one. */
51
- export function resolveSpecifier(fromFile, specifier, sourceRoot) {
52
- let base;
53
- if (specifier.startsWith("@/")) {
54
- base = path.resolve(sourceRoot, specifier.slice(2));
55
- }
56
- else if (specifier.startsWith(".")) {
57
- base = path.resolve(path.dirname(fromFile), specifier);
58
- }
59
- else {
60
- return undefined;
61
- }
62
- const candidates = [
63
- base,
64
- ...MODULE_EXTENSIONS.map((extension) => `${base}${extension}`),
65
- ...INDEX_BASENAMES.map((name) => path.join(base, name)),
66
- ];
67
- for (const candidate of candidates) {
68
- try {
69
- if (statSync(candidate).isFile())
70
- return candidate;
71
- }
72
- catch {
73
- /* Try the next candidate. */
74
- }
75
- }
76
- return undefined;
77
- }
78
- function build(sourceRoot, files) {
79
- const modules = new Map();
80
- const consumers = new Map();
81
- const symbolConsumers = new Map();
82
- for (const file of files) {
83
- try {
84
- modules.set(file, parseModule(file));
85
- }
86
- catch {
87
- /* A file that cannot be read or parsed contributes nothing to the graph. */
88
- }
89
- }
90
- for (const [file, module] of modules) {
91
- for (const moduleImport of module.imports) {
92
- const target = resolveSpecifier(file, moduleImport.specifier, sourceRoot);
93
- if (!target || !modules.has(target))
94
- continue;
95
- const fileConsumers = consumers.get(target) ?? new Set();
96
- fileConsumers.add(file);
97
- consumers.set(target, fileConsumers);
98
- for (const name of moduleImport.names) {
99
- const key = symbolKey(target, name);
100
- const nameConsumers = symbolConsumers.get(key) ?? new Set();
101
- nameConsumers.add(file);
102
- symbolConsumers.set(key, nameConsumers);
103
- }
104
- }
105
- }
106
- return { sourceRoot, modules, consumers, symbolConsumers };
107
- }
108
- let cache;
109
- /**
110
- * The project index for a source tree, rebuilt only when the tree changed.
111
- * Returns undefined when the tree does not exist, which is how a repository
112
- * without a `src/` folder opts out of every cross-file rule.
113
- */
114
- export function getProjectIndex(sourceRoot) {
115
- const now = Date.now();
116
- if (cache?.index.sourceRoot === sourceRoot && now - cache.checkedAt < REVALIDATE_AFTER_MS) {
117
- return cache.index;
118
- }
119
- try {
120
- if (!statSync(sourceRoot).isDirectory())
121
- return undefined;
122
- }
123
- catch {
124
- return undefined;
125
- }
126
- const files = listSourceFiles(sourceRoot).sort((left, right) => left.localeCompare(right));
127
- const currentFingerprint = fingerprint(files);
128
- if (cache?.index.sourceRoot === sourceRoot && cache.fingerprint === currentFingerprint) {
129
- cache.checkedAt = now;
130
- return cache.index;
131
- }
132
- const index = build(sourceRoot, files);
133
- cache = { index, checkedAt: now, fingerprint: currentFingerprint };
134
- return index;
135
- }
136
- /** Drops the memoized index. Used by tests that write a fresh tree per case. */
137
- export function clearProjectIndex() {
138
- cache = undefined;
139
- }