@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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/CHANGELOG.md +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -75
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that `ctx.compose()` calls match the declared `composes` array.
|
|
3
|
+
*
|
|
4
|
+
* Statically analyzes trail `blaze` functions to find `ctx.compose('trailId', ...)`
|
|
5
|
+
* calls and compares them against the `composes: [...]` declaration in the trail
|
|
6
|
+
* config. Reports errors for undeclared compositions and warnings for unused ones.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
findConfigProperty,
|
|
11
|
+
findBlazeBodies,
|
|
12
|
+
findTrailDefinitions,
|
|
13
|
+
offsetToLine,
|
|
14
|
+
parse,
|
|
15
|
+
walk,
|
|
16
|
+
} from './ast.js';
|
|
17
|
+
import type { AstNode } from './ast.js';
|
|
18
|
+
import { isTestFile } from './scan.js';
|
|
19
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared identifier helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Get the name of an Identifier node, or null. */
|
|
26
|
+
const identifierName = (node: AstNode | undefined): string | null => {
|
|
27
|
+
if (node?.type !== 'Identifier') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return (node as unknown as { name?: string }).name ?? null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// String literal helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Check if a node is a string literal (covers `StringLiteral` and `Literal` with string value). */
|
|
38
|
+
const isStringLiteral = (node: AstNode): boolean => {
|
|
39
|
+
if (node.type === 'StringLiteral') {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (node.type === 'Literal') {
|
|
43
|
+
return typeof (node as unknown as { value?: unknown }).value === 'string';
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Extract the string value from a string literal node. */
|
|
49
|
+
const getStringValue = (node: AstNode): string | null => {
|
|
50
|
+
const val = (node as unknown as { value?: unknown }).value;
|
|
51
|
+
return typeof val === 'string' ? val : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Const identifier resolution
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort resolution of `const NAME = 'value'` declarations via regex.
|
|
60
|
+
*
|
|
61
|
+
* Returns the string value if a simple `const <name> = '...'` or `"..."` is
|
|
62
|
+
* found in the source. Returns null for anything more complex.
|
|
63
|
+
*/
|
|
64
|
+
const deriveConstString = (name: string, sourceCode: string): string | null => {
|
|
65
|
+
const pattern = new RegExp(
|
|
66
|
+
`const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`
|
|
67
|
+
);
|
|
68
|
+
const match = pattern.exec(sourceCode);
|
|
69
|
+
if (!match) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return match[1] ?? match[2] ?? null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Try to resolve an Identifier element to a string via const declaration. */
|
|
76
|
+
const resolveIdentifierElement = (
|
|
77
|
+
el: AstNode,
|
|
78
|
+
sourceCode: string
|
|
79
|
+
): string | null => {
|
|
80
|
+
const name = identifierName(el);
|
|
81
|
+
if (!name) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return deriveConstString(name, sourceCode);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Resolve an array element to a static trail ID when possible. */
|
|
88
|
+
const deriveComposeElementId = (
|
|
89
|
+
element: AstNode,
|
|
90
|
+
sourceCode: string
|
|
91
|
+
): string | null => {
|
|
92
|
+
if (isStringLiteral(element)) {
|
|
93
|
+
return getStringValue(element);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (element.type === 'Identifier') {
|
|
97
|
+
return resolveIdentifierElement(element, sourceCode);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Declared composing extraction
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** Extract the ArrayExpression elements from a config's `composes` property. */
|
|
108
|
+
const getComposeElements = (config: AstNode): readonly AstNode[] | null => {
|
|
109
|
+
const composesProp = findConfigProperty(config, 'composes');
|
|
110
|
+
if (!composesProp) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const arrayNode = composesProp.value;
|
|
115
|
+
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const elements = (arrayNode as AstNode)['elements'] as
|
|
120
|
+
| readonly AstNode[]
|
|
121
|
+
| undefined;
|
|
122
|
+
return elements ?? null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
interface DeclaredComposes {
|
|
126
|
+
/** Statically resolved trail IDs from string literals / const identifiers. */
|
|
127
|
+
readonly ids: ReadonlySet<string>;
|
|
128
|
+
/**
|
|
129
|
+
* True if any element could not be statically resolved (e.g. trail object
|
|
130
|
+
* reference like `composes: [showGist]`). When true, "undeclared" diagnostics
|
|
131
|
+
* are softened from error to warn since the declared set is incomplete.
|
|
132
|
+
*/
|
|
133
|
+
readonly hasUnresolved: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Collect string IDs from array elements, resolving identifiers when possible.
|
|
138
|
+
*
|
|
139
|
+
* Trail-object references (`composes: [showGist]`) cannot be resolved at lint
|
|
140
|
+
* time; they're normalized at runtime by `trail()`. When any entry is
|
|
141
|
+
* unresolved, `hasUnresolved` is set so callers can soften diagnostics.
|
|
142
|
+
*/
|
|
143
|
+
/** Classify a single element and accumulate into the id set. */
|
|
144
|
+
const classifyComposeElement = (
|
|
145
|
+
element: AstNode,
|
|
146
|
+
sourceCode: string,
|
|
147
|
+
ids: Set<string>
|
|
148
|
+
): boolean => {
|
|
149
|
+
const resolved = deriveComposeElementId(element, sourceCode);
|
|
150
|
+
if (!resolved) {
|
|
151
|
+
// Element could not be statically resolved
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
ids.add(resolved);
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const resolveDeclaredComposeElements = (
|
|
159
|
+
elements: readonly AstNode[],
|
|
160
|
+
sourceCode: string
|
|
161
|
+
): DeclaredComposes => {
|
|
162
|
+
const ids = new Set<string>();
|
|
163
|
+
let hasUnresolved = false;
|
|
164
|
+
for (const element of elements) {
|
|
165
|
+
if (classifyComposeElement(element, sourceCode, ids)) {
|
|
166
|
+
hasUnresolved = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { hasUnresolved, ids };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** Extract declared composes from a `composes: [...]` array. */
|
|
173
|
+
const extractDeclaredComposes = (
|
|
174
|
+
config: AstNode,
|
|
175
|
+
sourceCode: string
|
|
176
|
+
): DeclaredComposes => {
|
|
177
|
+
const elements = getComposeElements(config);
|
|
178
|
+
return elements
|
|
179
|
+
? resolveDeclaredComposeElements(elements, sourceCode)
|
|
180
|
+
: { hasUnresolved: false, ids: new Set() };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Called composing extraction — member expression helpers
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
const MEMBER_TYPES = new Set(['StaticMemberExpression', 'MemberExpression']);
|
|
188
|
+
|
|
189
|
+
/** Extract object and property Identifier names from a MemberExpression. */
|
|
190
|
+
const extractMemberPair = (
|
|
191
|
+
callee: AstNode
|
|
192
|
+
): { objName: string; propName: string } | null => {
|
|
193
|
+
if (!MEMBER_TYPES.has(callee.type)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const objName = identifierName(
|
|
198
|
+
(callee as unknown as { object?: AstNode }).object
|
|
199
|
+
);
|
|
200
|
+
const propName = identifierName(
|
|
201
|
+
(callee as unknown as { property?: AstNode }).property
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return objName && propName ? { objName, propName } : null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract the second parameter name from a blaze function node.
|
|
209
|
+
*
|
|
210
|
+
* Handles `(input, ctx) => ...`, `async (input, context) => ...`,
|
|
211
|
+
* `function(input, ctx) { ... }`, and defaulted params like
|
|
212
|
+
* `(input, ctx = fallback) => ...` (AssignmentPattern whose `.left` is the
|
|
213
|
+
* Identifier).
|
|
214
|
+
*/
|
|
215
|
+
const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
216
|
+
const params = blazeBody['params'] as readonly AstNode[] | undefined;
|
|
217
|
+
if (!params || params.length < 2) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const [, param] = params;
|
|
221
|
+
if (param?.type === 'AssignmentPattern') {
|
|
222
|
+
const { left } = param as unknown as { left?: AstNode };
|
|
223
|
+
return identifierName(left);
|
|
224
|
+
}
|
|
225
|
+
return identifierName(param);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** Extract the local name bound to `compose` inside an ObjectPattern Property. */
|
|
229
|
+
const extractComposeLocalName = (prop: AstNode): string | null => {
|
|
230
|
+
if (prop.type !== 'Property') {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const { key, value } = prop as unknown as {
|
|
234
|
+
readonly key?: AstNode;
|
|
235
|
+
readonly value?: AstNode;
|
|
236
|
+
};
|
|
237
|
+
const keyName = identifierName(key);
|
|
238
|
+
if (keyName !== 'compose') {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return identifierName(value) ?? keyName;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** Collect `compose` local names from an ObjectPattern's properties. */
|
|
245
|
+
const collectComposeNamesFromPattern = (
|
|
246
|
+
pattern: AstNode,
|
|
247
|
+
names: Set<string>
|
|
248
|
+
): void => {
|
|
249
|
+
const { properties } = pattern as unknown as {
|
|
250
|
+
readonly properties?: readonly AstNode[];
|
|
251
|
+
};
|
|
252
|
+
for (const prop of properties ?? []) {
|
|
253
|
+
const localName = extractComposeLocalName(prop);
|
|
254
|
+
if (localName) {
|
|
255
|
+
names.add(localName);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** Check if a callee is a member-style compose call: <ctxName>.compose(...). */
|
|
261
|
+
const isMemberComposeCall = (
|
|
262
|
+
callee: AstNode,
|
|
263
|
+
ctxNames: ReadonlySet<string>
|
|
264
|
+
): boolean => {
|
|
265
|
+
const pair = extractMemberPair(callee);
|
|
266
|
+
return !!pair && ctxNames.has(pair.objName) && pair.propName === 'compose';
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
interface ExtractedComposeCall {
|
|
270
|
+
readonly ids: readonly string[];
|
|
271
|
+
readonly hasUnresolved: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const unresolvedCompose = (): ExtractedComposeCall => ({
|
|
275
|
+
hasUnresolved: true,
|
|
276
|
+
ids: [],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const resolveBatchComposeTupleTarget = (
|
|
280
|
+
element: AstNode,
|
|
281
|
+
sourceCode: string
|
|
282
|
+
): string | null => {
|
|
283
|
+
if (element.type !== 'ArrayExpression') {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const tupleElements = element['elements'] as readonly AstNode[] | undefined;
|
|
288
|
+
const [target] = tupleElements ?? [];
|
|
289
|
+
return target ? deriveComposeElementId(target, sourceCode) : null;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const collectBatchComposeId = (
|
|
293
|
+
element: AstNode,
|
|
294
|
+
sourceCode: string,
|
|
295
|
+
ids: string[]
|
|
296
|
+
): boolean => {
|
|
297
|
+
const resolved = resolveBatchComposeTupleTarget(element, sourceCode);
|
|
298
|
+
if (!resolved) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
ids.push(resolved);
|
|
302
|
+
return false;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** Extract statically-resolved trail IDs from `ctx.compose([[trail, input], ...])`. */
|
|
306
|
+
const extractBatchComposeIds = (
|
|
307
|
+
firstArg: AstNode | undefined,
|
|
308
|
+
sourceCode: string
|
|
309
|
+
): ExtractedComposeCall | null => {
|
|
310
|
+
if (firstArg?.type !== 'ArrayExpression') {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const elements = firstArg['elements'] as readonly AstNode[] | undefined;
|
|
315
|
+
const ids: string[] = [];
|
|
316
|
+
let hasUnresolved = false;
|
|
317
|
+
|
|
318
|
+
for (const element of elements ?? []) {
|
|
319
|
+
if (collectBatchComposeId(element, sourceCode, ids)) {
|
|
320
|
+
hasUnresolved = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { hasUnresolved, ids };
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const extractDirectComposeIds = (
|
|
328
|
+
firstArg: AstNode | undefined
|
|
329
|
+
): ExtractedComposeCall | null => {
|
|
330
|
+
if (!firstArg || !isStringLiteral(firstArg)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const value = getStringValue(firstArg);
|
|
335
|
+
return value ? { hasUnresolved: false, ids: [value] } : unresolvedCompose();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const isComposeCallExpression = (
|
|
339
|
+
callee: AstNode,
|
|
340
|
+
ctxNames: ReadonlySet<string>,
|
|
341
|
+
composeLocalNames: ReadonlySet<string>
|
|
342
|
+
): boolean =>
|
|
343
|
+
isMemberComposeCall(callee, ctxNames) ||
|
|
344
|
+
composeLocalNames.has(identifierName(callee) ?? '');
|
|
345
|
+
|
|
346
|
+
const extractComposeFirstArg = (node: AstNode): AstNode | undefined => {
|
|
347
|
+
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
348
|
+
return args?.[0];
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const resolveComposeCallNode = (
|
|
352
|
+
node: AstNode,
|
|
353
|
+
ctxNames: ReadonlySet<string>,
|
|
354
|
+
composeLocalNames: ReadonlySet<string>
|
|
355
|
+
): AstNode | null => {
|
|
356
|
+
if (node.type !== 'CallExpression') {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
361
|
+
if (
|
|
362
|
+
!callee ||
|
|
363
|
+
!isComposeCallExpression(callee, ctxNames, composeLocalNames)
|
|
364
|
+
) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return node;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const resolveComposeCallTargets = (
|
|
372
|
+
firstArg: AstNode | undefined,
|
|
373
|
+
sourceCode: string
|
|
374
|
+
): ExtractedComposeCall => {
|
|
375
|
+
const direct = extractDirectComposeIds(firstArg);
|
|
376
|
+
if (direct) {
|
|
377
|
+
return direct;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const batch = extractBatchComposeIds(firstArg, sourceCode);
|
|
381
|
+
return batch ?? unresolvedCompose();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if a node is a `<ctxName>.compose(...)` call and return any statically
|
|
386
|
+
* resolvable target IDs.
|
|
387
|
+
*
|
|
388
|
+
* Also matches bare `compose(...)` calls only when `compose` was verifiably
|
|
389
|
+
* destructured from the trail context. When the first argument is a non-string
|
|
390
|
+
* expression (e.g. a trail object identifier like `ctx.compose(showGist,
|
|
391
|
+
* input)`), marks the call as unresolved so callers can track that a compose
|
|
392
|
+
* call exists but its target cannot be statically resolved.
|
|
393
|
+
*/
|
|
394
|
+
const extractComposeCall = (
|
|
395
|
+
node: AstNode,
|
|
396
|
+
ctxNames: ReadonlySet<string>,
|
|
397
|
+
composeLocalNames: ReadonlySet<string>,
|
|
398
|
+
sourceCode: string
|
|
399
|
+
): ExtractedComposeCall | null => {
|
|
400
|
+
const composeCall = resolveComposeCallNode(node, ctxNames, composeLocalNames);
|
|
401
|
+
if (!composeCall) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return resolveComposeCallTargets(
|
|
406
|
+
extractComposeFirstArg(composeCall),
|
|
407
|
+
sourceCode
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Build the set of context parameter names to match against.
|
|
413
|
+
*
|
|
414
|
+
* Returns ONLY the actual second-parameter name from the blaze signature.
|
|
415
|
+
* No seeded defaults: if the blaze has no second parameter, the returned set
|
|
416
|
+
* is empty and no `ctx.compose(...)` / `context.compose(...)` calls are tracked
|
|
417
|
+
* for that blaze. An unrelated closure-scoped `ctx` identifier is not the
|
|
418
|
+
* trail context and must not be treated as one.
|
|
419
|
+
*
|
|
420
|
+
* Mirrors `fires-declarations.ts` and `resource-declarations.ts` for the same
|
|
421
|
+
* reason.
|
|
422
|
+
*/
|
|
423
|
+
const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
424
|
+
const ctxNames = new Set<string>();
|
|
425
|
+
const paramName = extractContextParamName(body);
|
|
426
|
+
if (paramName) {
|
|
427
|
+
ctxNames.add(paramName);
|
|
428
|
+
}
|
|
429
|
+
return ctxNames;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const getCtxDestructurePattern = (
|
|
433
|
+
node: AstNode,
|
|
434
|
+
ctxNames: ReadonlySet<string>
|
|
435
|
+
): AstNode | null => {
|
|
436
|
+
if (node.type !== 'VariableDeclarator') {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const { id, init } = node as unknown as {
|
|
440
|
+
readonly id?: AstNode;
|
|
441
|
+
readonly init?: AstNode;
|
|
442
|
+
};
|
|
443
|
+
if (!id || id.type !== 'ObjectPattern' || !init) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const initName = identifierName(init);
|
|
447
|
+
return initName && ctxNames.has(initName) ? id : null;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const getTopLevelStatements = (body: AstNode): readonly AstNode[] => {
|
|
451
|
+
const blockBody = (body as unknown as { body?: AstNode }).body;
|
|
452
|
+
if (!blockBody || blockBody.type !== 'BlockStatement') {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
return (blockBody as unknown as { body?: readonly AstNode[] }).body ?? [];
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const collectComposeNamesFromDeclaration = (
|
|
459
|
+
stmt: AstNode,
|
|
460
|
+
ctxNames: ReadonlySet<string>,
|
|
461
|
+
names: Set<string>
|
|
462
|
+
): void => {
|
|
463
|
+
if (stmt.type !== 'VariableDeclaration') {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const { kind } = stmt as unknown as { readonly kind?: string };
|
|
467
|
+
if (kind !== 'const') {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const declarations =
|
|
471
|
+
(stmt as unknown as { readonly declarations?: readonly AstNode[] })
|
|
472
|
+
.declarations ?? [];
|
|
473
|
+
for (const decl of declarations) {
|
|
474
|
+
const pattern = getCtxDestructurePattern(decl, ctxNames);
|
|
475
|
+
if (pattern) {
|
|
476
|
+
collectComposeNamesFromPattern(pattern, names);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const collectDestructuredComposeNames = (
|
|
482
|
+
body: AstNode,
|
|
483
|
+
ctxNames: ReadonlySet<string>
|
|
484
|
+
): ReadonlySet<string> => {
|
|
485
|
+
const names = new Set<string>();
|
|
486
|
+
for (const stmt of getTopLevelStatements(body)) {
|
|
487
|
+
collectComposeNamesFromDeclaration(stmt, ctxNames, names);
|
|
488
|
+
}
|
|
489
|
+
return names;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
interface CalledComposes {
|
|
493
|
+
/** Statically resolved trail IDs from string literal arguments. */
|
|
494
|
+
readonly ids: ReadonlySet<string>;
|
|
495
|
+
/**
|
|
496
|
+
* True if any `ctx.compose()` call used a non-string first argument (e.g.
|
|
497
|
+
* `ctx.compose(showGist, input)`). When true, "unused declaration"
|
|
498
|
+
* diagnostics are softened since the call may target a declared entry.
|
|
499
|
+
*/
|
|
500
|
+
readonly hasUnresolved: boolean;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Collect compose call results from a single blaze body. */
|
|
504
|
+
const collectComposeCallsFromBody = (
|
|
505
|
+
body: AstNode,
|
|
506
|
+
ids: Set<string>,
|
|
507
|
+
sourceCode: string
|
|
508
|
+
): boolean => {
|
|
509
|
+
const ctxNames = buildCtxNames(body);
|
|
510
|
+
const composeLocalNames = collectDestructuredComposeNames(body, ctxNames);
|
|
511
|
+
let foundUnresolved = false;
|
|
512
|
+
|
|
513
|
+
walk(body, (node) => {
|
|
514
|
+
const extracted = extractComposeCall(
|
|
515
|
+
node,
|
|
516
|
+
ctxNames,
|
|
517
|
+
composeLocalNames,
|
|
518
|
+
sourceCode
|
|
519
|
+
);
|
|
520
|
+
if (!extracted) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (extracted.hasUnresolved) {
|
|
525
|
+
foundUnresolved = true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
for (const id of extracted.ids) {
|
|
529
|
+
ids.add(id);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
return foundUnresolved;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/** Walk blaze bodies and collect all statically resolvable ctx.compose() trail IDs. */
|
|
537
|
+
const extractCalledComposes = (
|
|
538
|
+
config: AstNode,
|
|
539
|
+
sourceCode: string
|
|
540
|
+
): CalledComposes => {
|
|
541
|
+
const ids = new Set<string>();
|
|
542
|
+
let hasUnresolved = false;
|
|
543
|
+
|
|
544
|
+
for (const body of findBlazeBodies(config)) {
|
|
545
|
+
if (collectComposeCallsFromBody(body, ids, sourceCode)) {
|
|
546
|
+
hasUnresolved = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return { hasUnresolved, ids };
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// Diagnostic builders
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
const buildUndeclaredDiagnostic = (
|
|
558
|
+
trailId: string,
|
|
559
|
+
composedId: string,
|
|
560
|
+
filePath: string,
|
|
561
|
+
line: number,
|
|
562
|
+
softened = false
|
|
563
|
+
): WardenDiagnostic => ({
|
|
564
|
+
filePath,
|
|
565
|
+
line,
|
|
566
|
+
message: softened
|
|
567
|
+
? `Trail "${trailId}": ctx.compose('${composedId}') called but '${composedId}' is not declared in composes (may be declared via trail object references). Add the string id to composes, or use the same trail object form in both composes and ctx.compose(...).`
|
|
568
|
+
: `Trail "${trailId}": ctx.compose('${composedId}') called but '${composedId}' is not declared in composes. Add it to the trail composes array: composes: ['${composedId}', ...].`,
|
|
569
|
+
rule: 'composes-declarations',
|
|
570
|
+
severity: softened ? 'warn' : 'error',
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const buildUnusedDiagnostic = (
|
|
574
|
+
trailId: string,
|
|
575
|
+
composedId: string,
|
|
576
|
+
filePath: string,
|
|
577
|
+
line: number
|
|
578
|
+
): WardenDiagnostic => ({
|
|
579
|
+
filePath,
|
|
580
|
+
line,
|
|
581
|
+
message: `Trail "${trailId}": '${composedId}' declared in composes but ctx.compose('${composedId}') never called`,
|
|
582
|
+
rule: 'composes-declarations',
|
|
583
|
+
severity: 'warn',
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// Comparison
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
/** Emit error for each called ID not present in declared set. */
|
|
591
|
+
const reportUndeclared = (
|
|
592
|
+
called: ReadonlySet<string>,
|
|
593
|
+
declared: ReadonlySet<string>,
|
|
594
|
+
ctx: {
|
|
595
|
+
trailId: string;
|
|
596
|
+
filePath: string;
|
|
597
|
+
line: number;
|
|
598
|
+
softened?: boolean;
|
|
599
|
+
},
|
|
600
|
+
diagnostics: WardenDiagnostic[]
|
|
601
|
+
): void => {
|
|
602
|
+
for (const id of called) {
|
|
603
|
+
if (!declared.has(id)) {
|
|
604
|
+
diagnostics.push(
|
|
605
|
+
buildUndeclaredDiagnostic(
|
|
606
|
+
ctx.trailId,
|
|
607
|
+
id,
|
|
608
|
+
ctx.filePath,
|
|
609
|
+
ctx.line,
|
|
610
|
+
ctx.softened
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/** Emit warning for each declared ID not present in called set. */
|
|
618
|
+
const reportUnused = (
|
|
619
|
+
declared: ReadonlySet<string>,
|
|
620
|
+
called: ReadonlySet<string>,
|
|
621
|
+
ctx: { trailId: string; filePath: string; line: number },
|
|
622
|
+
diagnostics: WardenDiagnostic[]
|
|
623
|
+
): void => {
|
|
624
|
+
for (const id of declared) {
|
|
625
|
+
if (!called.has(id)) {
|
|
626
|
+
diagnostics.push(
|
|
627
|
+
buildUnusedDiagnostic(ctx.trailId, id, ctx.filePath, ctx.line)
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const checkTrailDefinition = (
|
|
634
|
+
def: { id: string; config: AstNode; start: number },
|
|
635
|
+
filePath: string,
|
|
636
|
+
sourceCode: string,
|
|
637
|
+
diagnostics: WardenDiagnostic[]
|
|
638
|
+
): void => {
|
|
639
|
+
const declared = extractDeclaredComposes(def.config, sourceCode);
|
|
640
|
+
const called = extractCalledComposes(def.config, sourceCode);
|
|
641
|
+
|
|
642
|
+
if (
|
|
643
|
+
declared.ids.size === 0 &&
|
|
644
|
+
!declared.hasUnresolved &&
|
|
645
|
+
called.ids.size === 0 &&
|
|
646
|
+
!called.hasUnresolved
|
|
647
|
+
) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const line = offsetToLine(sourceCode, def.start);
|
|
652
|
+
const ctx = { filePath, line, trailId: def.id };
|
|
653
|
+
|
|
654
|
+
// When the declared array contains trail object references we can't resolve,
|
|
655
|
+
// downgrade "undeclared" diagnostics from error to warn. The developer still
|
|
656
|
+
// sees genuinely undeclared calls, but we can't statically prove the call
|
|
657
|
+
// isn't covered by a trail object entry the runtime will normalize.
|
|
658
|
+
reportUndeclared(
|
|
659
|
+
called.ids,
|
|
660
|
+
declared.ids,
|
|
661
|
+
{ ...ctx, softened: declared.hasUnresolved },
|
|
662
|
+
diagnostics
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// When all ctx.compose() calls are statically resolved, report unused
|
|
666
|
+
// declarations. When some calls use trail object references (unresolved),
|
|
667
|
+
// skip — a declared string like 'gist.show' might be the target of an
|
|
668
|
+
// unresolved `ctx.compose(showGist)` call, producing false positives.
|
|
669
|
+
if (!called.hasUnresolved) {
|
|
670
|
+
reportUnused(declared.ids, called.ids, ctx, diagnostics);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// Rule
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Validates that `ctx.compose()` calls align with declared `composes` arrays.
|
|
680
|
+
*/
|
|
681
|
+
export const composesDeclarations: WardenRule = {
|
|
682
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
683
|
+
if (isTestFile(filePath)) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const ast = parse(filePath, sourceCode);
|
|
688
|
+
if (!ast) {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
693
|
+
|
|
694
|
+
for (const def of findTrailDefinitions(ast)) {
|
|
695
|
+
checkTrailDefinition(def, filePath, sourceCode, diagnostics);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return diagnostics;
|
|
699
|
+
},
|
|
700
|
+
description:
|
|
701
|
+
'Ensure ctx.compose() calls match the declared composes array in trail definitions.',
|
|
702
|
+
name: 'composes-declarations',
|
|
703
|
+
severity: 'error',
|
|
704
|
+
};
|