@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42
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 +837 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +149 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -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 +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -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/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -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-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -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 +89 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -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-cli-alias-export.trail.ts +41 -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 +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -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-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/trailhead-override-divergence.trail.ts +47 -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 +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +224 -0
- package/src/workspaces.ts +199 -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 -218
- 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 -161
- 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 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- 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 -47
- 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 -129
- 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 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- 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,625 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractStringOrTemplateLiteral,
|
|
3
|
+
findConfigProperty,
|
|
4
|
+
findImplementationBodies,
|
|
5
|
+
findTrailDefinitions,
|
|
6
|
+
getNodeArgument,
|
|
7
|
+
getNodeArguments,
|
|
8
|
+
getNodeCallee,
|
|
9
|
+
getNodeDiscriminant,
|
|
10
|
+
getNodeElements,
|
|
11
|
+
getNodeExpression,
|
|
12
|
+
getNodeId,
|
|
13
|
+
getNodeInit,
|
|
14
|
+
getNodeKey,
|
|
15
|
+
getNodeLeft,
|
|
16
|
+
getNodeObject,
|
|
17
|
+
getNodeOperator,
|
|
18
|
+
getNodeParams,
|
|
19
|
+
getNodeProperties,
|
|
20
|
+
getNodeProperty,
|
|
21
|
+
getNodeRight,
|
|
22
|
+
getNodeTest,
|
|
23
|
+
getNodeValueNode,
|
|
24
|
+
getPropertyName,
|
|
25
|
+
identifierName,
|
|
26
|
+
offsetToLine,
|
|
27
|
+
parse,
|
|
28
|
+
walkScope,
|
|
29
|
+
} from '@ontrails/source';
|
|
30
|
+
import type { AstNode } from '@ontrails/source';
|
|
31
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
32
|
+
|
|
33
|
+
const RULE_NAME = 'trail-fork-coaching';
|
|
34
|
+
|
|
35
|
+
const CONTROL_FIELD_NAMES = new Set(['action', 'operation']);
|
|
36
|
+
|
|
37
|
+
interface ControlField {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly node: AstNode;
|
|
40
|
+
readonly options: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SchemaBindingRecord {
|
|
44
|
+
readonly initializer: AstNode | undefined;
|
|
45
|
+
readonly scopeEnd: number;
|
|
46
|
+
readonly scopeStart: number;
|
|
47
|
+
readonly start: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type SchemaBindings = ReadonlyMap<string, readonly SchemaBindingRecord[]>;
|
|
51
|
+
|
|
52
|
+
interface ImplementationInputBindings {
|
|
53
|
+
readonly aliases: ReadonlyMap<string, string>;
|
|
54
|
+
readonly inputParamName: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const LEXICAL_SCOPE_TYPES = new Set([
|
|
58
|
+
'ArrowFunctionExpression',
|
|
59
|
+
'BlockStatement',
|
|
60
|
+
'CatchClause',
|
|
61
|
+
'ClassStaticBlock',
|
|
62
|
+
'ForInStatement',
|
|
63
|
+
'ForOfStatement',
|
|
64
|
+
'ForStatement',
|
|
65
|
+
'FunctionBody',
|
|
66
|
+
'FunctionDeclaration',
|
|
67
|
+
'FunctionExpression',
|
|
68
|
+
'Program',
|
|
69
|
+
'StaticBlock',
|
|
70
|
+
'SwitchStatement',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
74
|
+
let current = node;
|
|
75
|
+
while (
|
|
76
|
+
current?.type === 'TSAsExpression' ||
|
|
77
|
+
current?.type === 'TSSatisfiesExpression'
|
|
78
|
+
) {
|
|
79
|
+
current = getNodeExpression(current) ?? getNodeArgument(current);
|
|
80
|
+
}
|
|
81
|
+
return current;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const callArguments = (node: AstNode): readonly AstNode[] =>
|
|
85
|
+
getNodeArguments(node) ?? [];
|
|
86
|
+
|
|
87
|
+
const callName = (node: AstNode | undefined): string | null => {
|
|
88
|
+
const unwrapped = unwrapExpression(node);
|
|
89
|
+
if (unwrapped?.type !== 'CallExpression') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const callee = getNodeCallee(unwrapped);
|
|
93
|
+
if (callee?.type !== 'MemberExpression') {
|
|
94
|
+
return identifierName(callee);
|
|
95
|
+
}
|
|
96
|
+
return getPropertyName(getNodeProperty(callee));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const callObject = (node: AstNode): AstNode | undefined => {
|
|
100
|
+
const callee = getNodeCallee(node);
|
|
101
|
+
if (callee?.type !== 'MemberExpression') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return getNodeObject(callee);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const schemaBindingInitializer = (
|
|
108
|
+
schemaBindings: SchemaBindings,
|
|
109
|
+
name: string,
|
|
110
|
+
referenceStart: number
|
|
111
|
+
): AstNode | undefined => {
|
|
112
|
+
const records = schemaBindings.get(name) ?? [];
|
|
113
|
+
for (const record of records) {
|
|
114
|
+
if (
|
|
115
|
+
record.start < referenceStart &&
|
|
116
|
+
record.scopeStart <= referenceStart &&
|
|
117
|
+
referenceStart <= record.scopeEnd
|
|
118
|
+
) {
|
|
119
|
+
return record.initializer;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resolveSchemaReference = (
|
|
126
|
+
node: AstNode | undefined,
|
|
127
|
+
schemaBindings: SchemaBindings,
|
|
128
|
+
seenBindings = new Set<string>()
|
|
129
|
+
): AstNode | undefined => {
|
|
130
|
+
const current = unwrapExpression(node);
|
|
131
|
+
const name = identifierName(current);
|
|
132
|
+
if (!name || seenBindings.has(name)) {
|
|
133
|
+
return current;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const initializer = schemaBindingInitializer(
|
|
137
|
+
schemaBindings,
|
|
138
|
+
name,
|
|
139
|
+
current?.start ?? 0
|
|
140
|
+
);
|
|
141
|
+
if (!initializer) {
|
|
142
|
+
return current;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
seenBindings.add(name);
|
|
146
|
+
return resolveSchemaReference(initializer, schemaBindings, seenBindings);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const unwrapZodDecorators = (
|
|
150
|
+
node: AstNode | undefined,
|
|
151
|
+
schemaBindings: SchemaBindings
|
|
152
|
+
): AstNode | undefined => {
|
|
153
|
+
let current = resolveSchemaReference(node, schemaBindings);
|
|
154
|
+
while (current?.type === 'CallExpression') {
|
|
155
|
+
const name = callName(current);
|
|
156
|
+
if (
|
|
157
|
+
name !== 'default' &&
|
|
158
|
+
name !== 'describe' &&
|
|
159
|
+
name !== 'optional' &&
|
|
160
|
+
name !== 'nullable' &&
|
|
161
|
+
name !== 'nullish' &&
|
|
162
|
+
name !== 'passthrough' &&
|
|
163
|
+
name !== 'readonly' &&
|
|
164
|
+
name !== 'strict' &&
|
|
165
|
+
name !== 'strip'
|
|
166
|
+
) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
const next = callObject(current);
|
|
170
|
+
if (!next) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
current = resolveSchemaReference(next, schemaBindings);
|
|
174
|
+
}
|
|
175
|
+
return current;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const objectProperties = (node: AstNode): readonly AstNode[] =>
|
|
179
|
+
node.type === 'ObjectExpression' || node.type === 'ObjectPattern'
|
|
180
|
+
? (getNodeProperties(node) ?? [])
|
|
181
|
+
: [];
|
|
182
|
+
|
|
183
|
+
const propertyValue = (property: AstNode): AstNode | undefined =>
|
|
184
|
+
property.type === 'Property' ? getNodeValueNode(property) : undefined;
|
|
185
|
+
|
|
186
|
+
const arrayElements = (node: AstNode | undefined): readonly AstNode[] => {
|
|
187
|
+
const unwrapped = unwrapExpression(node);
|
|
188
|
+
if (unwrapped?.type !== 'ArrayExpression') {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
return getNodeElements(unwrapped).filter(
|
|
192
|
+
(element): element is AstNode => element !== null
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const literalOptionsFromEnum = (node: AstNode): readonly string[] => {
|
|
197
|
+
const [options] = callArguments(node);
|
|
198
|
+
return arrayElements(options)
|
|
199
|
+
.map((element) => extractStringOrTemplateLiteral(element))
|
|
200
|
+
.filter((value): value is string => value !== null);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const literalOptionFromLiteral = (node: AstNode): string | null => {
|
|
204
|
+
const [literal] = callArguments(node);
|
|
205
|
+
return extractStringOrTemplateLiteral(literal);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const literalOptionsFromUnion = (node: AstNode): readonly string[] => {
|
|
209
|
+
const [branches] = callArguments(node);
|
|
210
|
+
return arrayElements(branches)
|
|
211
|
+
.map((branch) => {
|
|
212
|
+
const base = unwrapZodDecorators(branch, new Map());
|
|
213
|
+
return base?.type === 'CallExpression' && callName(base) === 'literal'
|
|
214
|
+
? literalOptionFromLiteral(base)
|
|
215
|
+
: null;
|
|
216
|
+
})
|
|
217
|
+
.filter((value): value is string => value !== null);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const literalOptionsForSchema = (
|
|
221
|
+
schemaNode: AstNode | undefined,
|
|
222
|
+
schemaBindings: SchemaBindings
|
|
223
|
+
): readonly string[] => {
|
|
224
|
+
const base = unwrapZodDecorators(schemaNode, schemaBindings);
|
|
225
|
+
if (base?.type !== 'CallExpression') {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (callName(base) === 'enum') {
|
|
230
|
+
return literalOptionsFromEnum(base);
|
|
231
|
+
}
|
|
232
|
+
if (callName(base) === 'union') {
|
|
233
|
+
return literalOptionsFromUnion(base);
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const astChildNodes = (node: AstNode): readonly AstNode[] => {
|
|
239
|
+
const children: AstNode[] = [];
|
|
240
|
+
for (const value of Object.values(node)) {
|
|
241
|
+
if (Array.isArray(value)) {
|
|
242
|
+
children.push(
|
|
243
|
+
...value.filter(
|
|
244
|
+
(entry): entry is AstNode =>
|
|
245
|
+
typeof entry === 'object' &&
|
|
246
|
+
entry !== null &&
|
|
247
|
+
typeof (entry as AstNode).type === 'string'
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
typeof value === 'object' &&
|
|
254
|
+
value !== null &&
|
|
255
|
+
typeof (value as AstNode).type === 'string'
|
|
256
|
+
) {
|
|
257
|
+
children.push(value as AstNode);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return children;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const variableDeclaratorName = (node: AstNode): string | undefined => {
|
|
264
|
+
if (node.type !== 'VariableDeclarator') {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const id = getNodeId(node);
|
|
268
|
+
return identifierName(id) ?? undefined;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const collectSchemaBindings = (ast: AstNode): SchemaBindings => {
|
|
272
|
+
const bindings = new Map<string, SchemaBindingRecord[]>();
|
|
273
|
+
|
|
274
|
+
const visit = (
|
|
275
|
+
node: AstNode,
|
|
276
|
+
scope: { readonly end: number; readonly start: number }
|
|
277
|
+
): void => {
|
|
278
|
+
const nextScope = LEXICAL_SCOPE_TYPES.has(node.type)
|
|
279
|
+
? { end: node.end, start: node.start }
|
|
280
|
+
: scope;
|
|
281
|
+
const name = variableDeclaratorName(node);
|
|
282
|
+
if (name !== undefined) {
|
|
283
|
+
const records = bindings.get(name) ?? [];
|
|
284
|
+
records.push({
|
|
285
|
+
initializer: getNodeInit(node) ?? undefined,
|
|
286
|
+
scopeEnd: nextScope.end,
|
|
287
|
+
scopeStart: nextScope.start,
|
|
288
|
+
start: node.start,
|
|
289
|
+
});
|
|
290
|
+
bindings.set(name, records);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const child of astChildNodes(node)) {
|
|
294
|
+
visit(child, nextScope);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
visit(ast, { end: ast.end, start: ast.start });
|
|
299
|
+
return bindings;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const inputShapeObject = (
|
|
303
|
+
config: AstNode,
|
|
304
|
+
schemaBindings: SchemaBindings
|
|
305
|
+
): AstNode | null => {
|
|
306
|
+
const input = findConfigProperty(config, 'input');
|
|
307
|
+
const value = unwrapZodDecorators(
|
|
308
|
+
propertyValue(input ?? config),
|
|
309
|
+
schemaBindings
|
|
310
|
+
);
|
|
311
|
+
if (value?.type !== 'CallExpression' || callName(value) !== 'object') {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const [shape] = callArguments(value);
|
|
316
|
+
const unwrappedShape = unwrapExpression(shape);
|
|
317
|
+
return unwrappedShape?.type === 'ObjectExpression' ? unwrappedShape : null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const findControlFields = (
|
|
321
|
+
config: AstNode,
|
|
322
|
+
schemaBindings: SchemaBindings
|
|
323
|
+
): readonly ControlField[] => {
|
|
324
|
+
const shape = inputShapeObject(config, schemaBindings);
|
|
325
|
+
if (!shape) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fields: ControlField[] = [];
|
|
330
|
+
for (const property of objectProperties(shape)) {
|
|
331
|
+
const name = getPropertyName(getNodeKey(property));
|
|
332
|
+
if (!name || !CONTROL_FIELD_NAMES.has(name)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const options = literalOptionsForSchema(
|
|
337
|
+
propertyValue(property),
|
|
338
|
+
schemaBindings
|
|
339
|
+
);
|
|
340
|
+
if (options.length >= 2) {
|
|
341
|
+
fields.push({ name, node: property, options });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return fields;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const implementationParams = (implementation: AstNode): readonly AstNode[] =>
|
|
348
|
+
getNodeParams(implementation) ?? [];
|
|
349
|
+
|
|
350
|
+
const memberFieldName = (
|
|
351
|
+
node: AstNode | undefined,
|
|
352
|
+
inputParamName: string | null
|
|
353
|
+
): string | null => {
|
|
354
|
+
if (!inputParamName) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const unwrapped = unwrapExpression(node);
|
|
358
|
+
if (unwrapped?.type !== 'MemberExpression') {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const object = getNodeObject(unwrapped);
|
|
362
|
+
if (identifierName(object) !== inputParamName) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return getPropertyName(getNodeProperty(unwrapped));
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const patternAliasName = (node: AstNode | undefined): string | null => {
|
|
369
|
+
const unwrapped = unwrapExpression(node);
|
|
370
|
+
if (unwrapped?.type === 'AssignmentPattern') {
|
|
371
|
+
return identifierName(getNodeLeft(unwrapped));
|
|
372
|
+
}
|
|
373
|
+
return identifierName(unwrapped);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const collectDestructuredFieldAliasesFromPattern = (
|
|
377
|
+
pattern: AstNode | undefined,
|
|
378
|
+
fields: ReadonlySet<string>
|
|
379
|
+
): ReadonlyMap<string, string> => {
|
|
380
|
+
const aliases = new Map<string, string>();
|
|
381
|
+
if (pattern?.type !== 'ObjectPattern') {
|
|
382
|
+
return aliases;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const property of objectProperties(pattern)) {
|
|
386
|
+
if (property.type === 'RestElement') {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const fieldName = getPropertyName(getNodeKey(property));
|
|
390
|
+
if (!fieldName || !fields.has(fieldName)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const alias = patternAliasName(propertyValue(property)) ?? fieldName;
|
|
394
|
+
aliases.set(alias, fieldName);
|
|
395
|
+
}
|
|
396
|
+
return aliases;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const collectDestructuredFieldAliases = (
|
|
400
|
+
implementation: AstNode,
|
|
401
|
+
inputParamName: string,
|
|
402
|
+
fields: ReadonlySet<string>
|
|
403
|
+
): ReadonlyMap<string, string> => {
|
|
404
|
+
const aliases = new Map<string, string>();
|
|
405
|
+
|
|
406
|
+
walkScope(implementation, (node) => {
|
|
407
|
+
if (node.type !== 'VariableDeclarator') {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const id = getNodeId(node);
|
|
411
|
+
const init = getNodeInit(node);
|
|
412
|
+
if (
|
|
413
|
+
identifierName(init) !== inputParamName ||
|
|
414
|
+
id?.type !== 'ObjectPattern'
|
|
415
|
+
) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const [alias, fieldName] of collectDestructuredFieldAliasesFromPattern(
|
|
420
|
+
id,
|
|
421
|
+
fields
|
|
422
|
+
)) {
|
|
423
|
+
aliases.set(alias, fieldName);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return aliases;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const collectImplementationInputBindings = (
|
|
431
|
+
implementation: AstNode,
|
|
432
|
+
fields: ReadonlySet<string>
|
|
433
|
+
): ImplementationInputBindings | null => {
|
|
434
|
+
const [inputParam] = implementationParams(implementation);
|
|
435
|
+
const inputParamName = identifierName(inputParam);
|
|
436
|
+
if (inputParamName) {
|
|
437
|
+
return {
|
|
438
|
+
aliases: collectDestructuredFieldAliases(
|
|
439
|
+
implementation,
|
|
440
|
+
inputParamName,
|
|
441
|
+
fields
|
|
442
|
+
),
|
|
443
|
+
inputParamName,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (inputParam?.type === 'ObjectPattern') {
|
|
448
|
+
return {
|
|
449
|
+
aliases: collectDestructuredFieldAliasesFromPattern(inputParam, fields),
|
|
450
|
+
inputParamName: null,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const branchFieldName = (
|
|
458
|
+
node: AstNode | undefined,
|
|
459
|
+
inputParamName: string | null,
|
|
460
|
+
aliases: ReadonlyMap<string, string>
|
|
461
|
+
): string | null => {
|
|
462
|
+
const memberName = memberFieldName(node, inputParamName);
|
|
463
|
+
if (memberName) {
|
|
464
|
+
return memberName;
|
|
465
|
+
}
|
|
466
|
+
const identifier = identifierName(unwrapExpression(node));
|
|
467
|
+
return identifier ? (aliases.get(identifier) ?? null) : null;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const comparisonBranchesOnField = (
|
|
471
|
+
node: AstNode | undefined,
|
|
472
|
+
inputParamName: string | null,
|
|
473
|
+
fieldName: string,
|
|
474
|
+
aliases: ReadonlyMap<string, string>
|
|
475
|
+
): AstNode | null => {
|
|
476
|
+
const unwrapped = unwrapExpression(node);
|
|
477
|
+
if (!unwrapped) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (unwrapped.type === 'LogicalExpression') {
|
|
482
|
+
const left = getNodeLeft(unwrapped);
|
|
483
|
+
const right = getNodeRight(unwrapped);
|
|
484
|
+
return (
|
|
485
|
+
comparisonBranchesOnField(left, inputParamName, fieldName, aliases) ??
|
|
486
|
+
comparisonBranchesOnField(right, inputParamName, fieldName, aliases)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (unwrapped.type === 'UnaryExpression') {
|
|
491
|
+
const argument = getNodeArgument(unwrapped);
|
|
492
|
+
return comparisonBranchesOnField(
|
|
493
|
+
argument,
|
|
494
|
+
inputParamName,
|
|
495
|
+
fieldName,
|
|
496
|
+
aliases
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (unwrapped.type !== 'BinaryExpression') {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const left = getNodeLeft(unwrapped);
|
|
505
|
+
const operator = getNodeOperator(unwrapped);
|
|
506
|
+
const right = getNodeRight(unwrapped);
|
|
507
|
+
if (operator !== '===' && operator !== '!==') {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return branchFieldName(left, inputParamName, aliases) === fieldName ||
|
|
512
|
+
branchFieldName(right, inputParamName, aliases) === fieldName
|
|
513
|
+
? unwrapped
|
|
514
|
+
: null;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const branchesOnField = (
|
|
518
|
+
implementation: AstNode,
|
|
519
|
+
inputParamName: string | null,
|
|
520
|
+
fieldName: string,
|
|
521
|
+
aliases: ReadonlyMap<string, string>
|
|
522
|
+
): AstNode | null => {
|
|
523
|
+
let found: AstNode | null = null;
|
|
524
|
+
walkScope(implementation, (node) => {
|
|
525
|
+
if (found) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (node.type === 'SwitchStatement') {
|
|
530
|
+
const discriminant = getNodeDiscriminant(node);
|
|
531
|
+
if (
|
|
532
|
+
branchFieldName(discriminant, inputParamName, aliases) === fieldName
|
|
533
|
+
) {
|
|
534
|
+
found = node;
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (node.type !== 'IfStatement' && node.type !== 'ConditionalExpression') {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const test = getNodeTest(node);
|
|
543
|
+
found = comparisonBranchesOnField(test, inputParamName, fieldName, aliases);
|
|
544
|
+
});
|
|
545
|
+
return found;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const diagnosticForField = (
|
|
549
|
+
sourceCode: string,
|
|
550
|
+
filePath: string,
|
|
551
|
+
trailId: string,
|
|
552
|
+
field: ControlField,
|
|
553
|
+
branchNode: AstNode
|
|
554
|
+
): WardenDiagnostic => ({
|
|
555
|
+
filePath,
|
|
556
|
+
line: offsetToLine(sourceCode, branchNode.start),
|
|
557
|
+
message: `Trail "${trailId}" branches on input.${field.name} (${field.options
|
|
558
|
+
.map((option) => `"${option}"`)
|
|
559
|
+
.join(
|
|
560
|
+
', '
|
|
561
|
+
)}). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a trailhead that preserves member identity.`,
|
|
562
|
+
rule: RULE_NAME,
|
|
563
|
+
severity: 'warn',
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
export const trailForkCoaching: WardenRule = {
|
|
567
|
+
check(sourceCode, filePath) {
|
|
568
|
+
const ast = parse(filePath, sourceCode);
|
|
569
|
+
if (!ast) {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const schemaBindings = collectSchemaBindings(ast);
|
|
574
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
575
|
+
for (const definition of findTrailDefinitions(ast)) {
|
|
576
|
+
if (definition.kind !== 'trail') {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const fields = findControlFields(definition.config, schemaBindings);
|
|
581
|
+
if (fields.length === 0) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const fieldNames = new Set(fields.map((field) => field.name));
|
|
585
|
+
|
|
586
|
+
for (const implementation of findImplementationBodies(
|
|
587
|
+
definition.config
|
|
588
|
+
)) {
|
|
589
|
+
const inputBindings = collectImplementationInputBindings(
|
|
590
|
+
implementation,
|
|
591
|
+
fieldNames
|
|
592
|
+
);
|
|
593
|
+
if (!inputBindings) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
for (const field of fields) {
|
|
598
|
+
const branchNode = branchesOnField(
|
|
599
|
+
implementation,
|
|
600
|
+
inputBindings.inputParamName,
|
|
601
|
+
field.name,
|
|
602
|
+
inputBindings.aliases
|
|
603
|
+
);
|
|
604
|
+
if (branchNode) {
|
|
605
|
+
diagnostics.push(
|
|
606
|
+
diagnosticForField(
|
|
607
|
+
sourceCode,
|
|
608
|
+
filePath,
|
|
609
|
+
definition.id,
|
|
610
|
+
field,
|
|
611
|
+
branchNode
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return diagnostics;
|
|
620
|
+
},
|
|
621
|
+
description:
|
|
622
|
+
'Coach trails away from hiding several capabilities behind one branching action or operation input.',
|
|
623
|
+
name: RULE_NAME,
|
|
624
|
+
severity: 'warn',
|
|
625
|
+
};
|