@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,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findConfigProperty,
|
|
3
|
+
findTrailDefinitions,
|
|
4
|
+
identifierName,
|
|
5
|
+
offsetToLine,
|
|
6
|
+
parse,
|
|
7
|
+
walk,
|
|
8
|
+
walkScope,
|
|
9
|
+
} from '@ontrails/source';
|
|
10
|
+
import type { AstNode } from '@ontrails/source';
|
|
11
|
+
import { isTestFile } from './scan.js';
|
|
12
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
13
|
+
|
|
14
|
+
const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
15
|
+
'ParenthesizedExpression',
|
|
16
|
+
'TSAsExpression',
|
|
17
|
+
'TSSatisfiesExpression',
|
|
18
|
+
'TSNonNullExpression',
|
|
19
|
+
'TSTypeAssertion',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const FUNCTION_TYPES = new Set([
|
|
23
|
+
'ArrowFunctionExpression',
|
|
24
|
+
'FunctionDeclaration',
|
|
25
|
+
'FunctionExpression',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
29
|
+
let current = node;
|
|
30
|
+
while (current && TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
31
|
+
current = current['expression'] as AstNode | undefined;
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getDetourElements = (config: AstNode): readonly (AstNode | null)[] => {
|
|
37
|
+
const detoursProp = findConfigProperty(config, 'detours');
|
|
38
|
+
if (!detoursProp) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const detoursValue = unwrapExpression(
|
|
43
|
+
detoursProp.value as AstNode | undefined
|
|
44
|
+
);
|
|
45
|
+
if (!detoursValue || detoursValue.type !== 'ArrayExpression') {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
((detoursValue as AstNode)['elements'] as readonly (AstNode | null)[]) ?? []
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getFunctionBody = (node: AstNode | undefined): AstNode | undefined => {
|
|
55
|
+
if (!node || !FUNCTION_TYPES.has(node.type)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { body } = node;
|
|
60
|
+
return Array.isArray(body) ? undefined : (body as AstNode | undefined);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
interface RecoverBodyMatch {
|
|
64
|
+
readonly index: number;
|
|
65
|
+
readonly trailId: string;
|
|
66
|
+
readonly body: AstNode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const collectFunctionRecoverBinding = (
|
|
70
|
+
bindings: Map<string, AstNode>,
|
|
71
|
+
node: AstNode
|
|
72
|
+
): boolean => {
|
|
73
|
+
if (node.type !== 'FunctionDeclaration') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const name = identifierName(node['id'] as AstNode | undefined);
|
|
78
|
+
if (name) {
|
|
79
|
+
bindings.set(name, node);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const collectVariableRecoverBinding = (
|
|
85
|
+
bindings: Map<string, AstNode>,
|
|
86
|
+
node: AstNode
|
|
87
|
+
): void => {
|
|
88
|
+
if (node.type !== 'VariableDeclarator') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const name = identifierName(node['id'] as AstNode | undefined);
|
|
93
|
+
const init = unwrapExpression(node['init'] as AstNode | undefined);
|
|
94
|
+
if (!name || !init || !FUNCTION_TYPES.has(init.type)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
bindings.set(name, init);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const collectRecoverBinding = (
|
|
102
|
+
bindings: Map<string, AstNode>,
|
|
103
|
+
node: AstNode
|
|
104
|
+
): void => {
|
|
105
|
+
if (collectFunctionRecoverBinding(bindings, node)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
collectVariableRecoverBinding(bindings, node);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const collectRecoverBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
113
|
+
const bindings = new Map<string, AstNode>();
|
|
114
|
+
|
|
115
|
+
walk(ast, (node) => {
|
|
116
|
+
collectRecoverBinding(bindings, node);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return bindings;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const resolveRecoverBody = (
|
|
123
|
+
node: AstNode | undefined,
|
|
124
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
125
|
+
): AstNode | undefined => {
|
|
126
|
+
const unwrapped = unwrapExpression(node);
|
|
127
|
+
if (!unwrapped) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const inlineBody = getFunctionBody(unwrapped);
|
|
132
|
+
if (inlineBody) {
|
|
133
|
+
return inlineBody;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const bindingName = identifierName(unwrapped);
|
|
137
|
+
if (!bindingName) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return getFunctionBody(bindings.get(bindingName));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const resolveRecoverBodyFromElement = (
|
|
145
|
+
element: AstNode | null,
|
|
146
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
147
|
+
): AstNode | undefined => {
|
|
148
|
+
if (!element || element.type !== 'ObjectExpression') {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const recoverProp = findConfigProperty(element, 'recover');
|
|
153
|
+
return resolveRecoverBody(
|
|
154
|
+
recoverProp?.value as AstNode | undefined,
|
|
155
|
+
bindings
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const appendRecoverBodies = (
|
|
160
|
+
bodies: RecoverBodyMatch[],
|
|
161
|
+
definition: { readonly config: AstNode; readonly id: string },
|
|
162
|
+
bindings: ReadonlyMap<string, AstNode>
|
|
163
|
+
): void => {
|
|
164
|
+
const detourElements = getDetourElements(definition.config);
|
|
165
|
+
for (const [index, element] of detourElements.entries()) {
|
|
166
|
+
const body = resolveRecoverBodyFromElement(element, bindings);
|
|
167
|
+
if (!body) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
bodies.push({ body, index, trailId: definition.id });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const findRecoverBodies = (ast: AstNode): readonly RecoverBodyMatch[] => {
|
|
176
|
+
const bindings = collectRecoverBindings(ast);
|
|
177
|
+
const bodies: RecoverBodyMatch[] = [];
|
|
178
|
+
|
|
179
|
+
for (const definition of findTrailDefinitions(ast)) {
|
|
180
|
+
if (definition.kind !== 'trail') {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
appendRecoverBodies(bodies, definition, bindings);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return bodies;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const noThrowInDetourRecover: WardenRule = {
|
|
191
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
192
|
+
if (isTestFile(filePath)) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ast = parse(filePath, sourceCode);
|
|
197
|
+
if (!ast) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
202
|
+
|
|
203
|
+
for (const recover of findRecoverBodies(ast)) {
|
|
204
|
+
walkScope(recover.body, (node) => {
|
|
205
|
+
if (node.type !== 'ThrowStatement') {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
filePath,
|
|
211
|
+
line: offsetToLine(sourceCode, node.start),
|
|
212
|
+
message: `Trail "${recover.trailId}" detour[${recover.index}] recover must not throw. Return Result.err() instead.`,
|
|
213
|
+
rule: 'no-throw-in-detour-recover',
|
|
214
|
+
severity: 'error',
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return diagnostics;
|
|
220
|
+
},
|
|
221
|
+
description:
|
|
222
|
+
'Disallow throw statements inside detour recover functions. Use Result.err() instead.',
|
|
223
|
+
name: 'no-throw-in-detour-recover',
|
|
224
|
+
severity: 'error',
|
|
225
|
+
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Finds `throw` statements inside `
|
|
2
|
+
* Finds `throw` statements inside `implementation:` function bodies.
|
|
3
3
|
*
|
|
4
|
-
* Uses AST
|
|
5
|
-
*
|
|
4
|
+
* Uses scope-aware AST walking so throws inside nested callbacks
|
|
5
|
+
* (e.g. `.map()`, `.filter()`, inner helpers) are not attributed to
|
|
6
|
+
* the implementation body itself. ADR-0007 requires this class of false positive
|
|
7
|
+
* to be avoided — only throws in the implementation body scope should be flagged.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findImplementationBodies,
|
|
12
|
+
offsetToLine,
|
|
13
|
+
parse,
|
|
14
|
+
walkScope,
|
|
15
|
+
} from '@ontrails/source';
|
|
9
16
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
10
17
|
|
|
11
18
|
export const noThrowInImplementation: WardenRule = {
|
|
@@ -17,14 +24,14 @@ export const noThrowInImplementation: WardenRule = {
|
|
|
17
24
|
|
|
18
25
|
const diagnostics: WardenDiagnostic[] = [];
|
|
19
26
|
|
|
20
|
-
for (const body of
|
|
21
|
-
|
|
27
|
+
for (const body of findImplementationBodies(ast)) {
|
|
28
|
+
walkScope(body, (node) => {
|
|
22
29
|
if (node.type === 'ThrowStatement') {
|
|
23
30
|
diagnostics.push({
|
|
24
31
|
filePath,
|
|
25
32
|
line: offsetToLine(sourceCode, node.start),
|
|
26
33
|
message:
|
|
27
|
-
'Do not throw inside implementation. Use Result.err() instead.',
|
|
34
|
+
'Do not throw inside the implementation. Use Result.err() instead.',
|
|
28
35
|
rule: 'no-throw-in-implementation',
|
|
29
36
|
severity: 'error',
|
|
30
37
|
});
|
|
@@ -35,7 +42,7 @@ export const noThrowInImplementation: WardenRule = {
|
|
|
35
42
|
return diagnostics;
|
|
36
43
|
},
|
|
37
44
|
description:
|
|
38
|
-
'Disallow throw statements inside
|
|
45
|
+
'Disallow throw statements inside implementation bodies. Use Result.err() instead.',
|
|
39
46
|
name: 'no-throw-in-implementation',
|
|
40
47
|
severity: 'error',
|
|
41
48
|
};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getNodeArgument,
|
|
3
|
+
getNodeBodyStatements,
|
|
4
|
+
getNodeCallee,
|
|
5
|
+
getNodeComputed,
|
|
6
|
+
getNodeDeclaration,
|
|
7
|
+
getNodeDeclarations,
|
|
8
|
+
getNodeExported,
|
|
9
|
+
getNodeExpression,
|
|
10
|
+
getNodeId,
|
|
11
|
+
getNodeImported,
|
|
12
|
+
getNodeInit,
|
|
13
|
+
getNodeLeft,
|
|
14
|
+
getNodeLocal,
|
|
15
|
+
getNodeObject,
|
|
16
|
+
getNodeProperty,
|
|
17
|
+
getNodeSource,
|
|
18
|
+
getNodeSpecifiers,
|
|
19
|
+
getStringValue,
|
|
20
|
+
identifierName,
|
|
21
|
+
isStringLiteral,
|
|
22
|
+
offsetToLine,
|
|
23
|
+
parse,
|
|
24
|
+
} from '@ontrails/source';
|
|
25
|
+
import type { AstNode } from '@ontrails/source';
|
|
26
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
27
|
+
|
|
28
|
+
const RULE_NAME = 'no-top-level-surface';
|
|
29
|
+
|
|
30
|
+
const TOPO_EXPORT_NAMES = new Set(['app', 'graph']);
|
|
31
|
+
const SURFACE_OPEN_CALLEE_NAMES = new Set([
|
|
32
|
+
'connectStdio',
|
|
33
|
+
'startServer',
|
|
34
|
+
'surface',
|
|
35
|
+
]);
|
|
36
|
+
const TOPO_IMPORT_SOURCES = new Set(['@ontrails/core']);
|
|
37
|
+
const SURFACE_IMPORT_SOURCES = new Set([
|
|
38
|
+
'@ontrails/commander',
|
|
39
|
+
'@ontrails/hono',
|
|
40
|
+
'@ontrails/http',
|
|
41
|
+
'@ontrails/http/bun',
|
|
42
|
+
'@ontrails/mcp',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const diagnosticMessage =
|
|
46
|
+
'This module exports a topo and opens a surface at module top level. Trails introspection commands (`survey`, `guide`, `compile`) import topo entry modules, so opening a surface here can trigger sockets or transports during introspection. Move surface-opening to a separate entry/bin and keep the topo-export module side-effect-free.';
|
|
47
|
+
|
|
48
|
+
const unwrapExportDeclaration = (node: AstNode): AstNode =>
|
|
49
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
50
|
+
node.type === 'ExportDefaultDeclaration'
|
|
51
|
+
? (getNodeDeclaration(node) ?? node)
|
|
52
|
+
: node;
|
|
53
|
+
|
|
54
|
+
interface ImportedBindings {
|
|
55
|
+
readonly named: ReadonlyMap<string, string>;
|
|
56
|
+
readonly namespaces: ReadonlySet<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const importSource = (node: AstNode): string | null => {
|
|
60
|
+
const source = getNodeSource(node);
|
|
61
|
+
return source && isStringLiteral(source) ? getStringValue(source) : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const importedSpecifierName = (node: AstNode | undefined): string | null =>
|
|
65
|
+
identifierName(node) ??
|
|
66
|
+
(node && isStringLiteral(node) ? getStringValue(node) : null);
|
|
67
|
+
|
|
68
|
+
const addFrameworkImportBindings = (
|
|
69
|
+
ast: AstNode,
|
|
70
|
+
sources: ReadonlySet<string>,
|
|
71
|
+
allowedImports: ReadonlySet<string>
|
|
72
|
+
): ImportedBindings => {
|
|
73
|
+
const named = new Map<string, string>();
|
|
74
|
+
const namespaces = new Set<string>();
|
|
75
|
+
const body = getNodeBodyStatements(ast);
|
|
76
|
+
|
|
77
|
+
for (const statement of body) {
|
|
78
|
+
if (
|
|
79
|
+
statement.type !== 'ImportDeclaration' ||
|
|
80
|
+
!sources.has(importSource(statement) ?? '')
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const specifiers = getNodeSpecifiers(statement);
|
|
86
|
+
for (const specifier of specifiers) {
|
|
87
|
+
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
88
|
+
const localName = identifierName(getNodeLocal(specifier));
|
|
89
|
+
if (localName) {
|
|
90
|
+
namespaces.add(localName);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (specifier.type !== 'ImportSpecifier') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const imported = getNodeImported(specifier);
|
|
99
|
+
const local = getNodeLocal(specifier);
|
|
100
|
+
const importedName = importedSpecifierName(imported);
|
|
101
|
+
const localName = identifierName(local);
|
|
102
|
+
if (importedName && allowedImports.has(importedName) && localName) {
|
|
103
|
+
named.set(localName, importedName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { named, namespaces };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
112
|
+
let current = node;
|
|
113
|
+
while (
|
|
114
|
+
current?.type === 'AwaitExpression' ||
|
|
115
|
+
current?.type === 'ChainExpression' ||
|
|
116
|
+
current?.type === 'TSAsExpression' ||
|
|
117
|
+
current?.type === 'TSSatisfiesExpression'
|
|
118
|
+
) {
|
|
119
|
+
current = getNodeExpression(current) ?? getNodeArgument(current);
|
|
120
|
+
}
|
|
121
|
+
return current;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const memberExpressionParts = (
|
|
125
|
+
node: AstNode | undefined
|
|
126
|
+
): { objectName: string | null; propertyName: string | null } => {
|
|
127
|
+
if (node?.type !== 'MemberExpression') {
|
|
128
|
+
return { objectName: null, propertyName: null };
|
|
129
|
+
}
|
|
130
|
+
const computed = getNodeComputed(node);
|
|
131
|
+
const property = getNodeProperty(node);
|
|
132
|
+
return {
|
|
133
|
+
objectName: identifierName(getNodeObject(node)),
|
|
134
|
+
propertyName: computed ? null : identifierName(property),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const calleeName = (
|
|
139
|
+
node: AstNode | undefined,
|
|
140
|
+
bindings: ImportedBindings
|
|
141
|
+
): string | null => {
|
|
142
|
+
const callee = unwrapExpression(node);
|
|
143
|
+
if (!callee) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const directName = identifierName(callee);
|
|
147
|
+
if (directName && bindings.named.has(directName)) {
|
|
148
|
+
return bindings.named.get(directName) ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { objectName, propertyName } = memberExpressionParts(callee);
|
|
152
|
+
if (
|
|
153
|
+
objectName &&
|
|
154
|
+
propertyName &&
|
|
155
|
+
bindings.namespaces.has(objectName) &&
|
|
156
|
+
(SURFACE_OPEN_CALLEE_NAMES.has(propertyName) || propertyName === 'topo')
|
|
157
|
+
) {
|
|
158
|
+
return propertyName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const isTopoCall = (
|
|
165
|
+
node: AstNode | undefined,
|
|
166
|
+
bindings: ImportedBindings
|
|
167
|
+
): boolean =>
|
|
168
|
+
calleeName(getNodeCallee(unwrapExpression(node)), bindings) === 'topo';
|
|
169
|
+
|
|
170
|
+
const isSurfaceOpenCall = (
|
|
171
|
+
node: AstNode | undefined,
|
|
172
|
+
bindings: ImportedBindings
|
|
173
|
+
): boolean => {
|
|
174
|
+
const expression = unwrapExpression(node);
|
|
175
|
+
if (expression?.type !== 'CallExpression') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const callee = getNodeCallee(expression);
|
|
180
|
+
const directName = calleeName(callee, bindings);
|
|
181
|
+
if (directName && SURFACE_OPEN_CALLEE_NAMES.has(directName)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
const { objectName, propertyName } = memberExpressionParts(callee);
|
|
185
|
+
if (
|
|
186
|
+
objectName &&
|
|
187
|
+
propertyName === 'listen' &&
|
|
188
|
+
bindings.namespaces.has(objectName)
|
|
189
|
+
) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const declarationIdName = (node: AstNode | undefined): string | null =>
|
|
197
|
+
identifierName(node) ?? identifierName(node ? getNodeLeft(node) : undefined);
|
|
198
|
+
|
|
199
|
+
const collectTopLevelTopoBindings = (
|
|
200
|
+
ast: AstNode,
|
|
201
|
+
topoBindings: ImportedBindings
|
|
202
|
+
): ReadonlySet<string> => {
|
|
203
|
+
const bindings = new Set<string>();
|
|
204
|
+
const body = getNodeBodyStatements(ast);
|
|
205
|
+
|
|
206
|
+
for (const statement of body) {
|
|
207
|
+
const declaration = unwrapExportDeclaration(statement);
|
|
208
|
+
if (declaration.type === 'VariableDeclaration') {
|
|
209
|
+
const declarations = getNodeDeclarations(declaration);
|
|
210
|
+
for (const item of declarations) {
|
|
211
|
+
const id = getNodeId(item);
|
|
212
|
+
const init = getNodeInit(item);
|
|
213
|
+
const name = declarationIdName(id);
|
|
214
|
+
if (name && isTopoCall(init, topoBindings)) {
|
|
215
|
+
bindings.add(name);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return bindings;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const namedExportCarriesTopo = (
|
|
225
|
+
statement: AstNode,
|
|
226
|
+
topLevelTopoBindings: ReadonlySet<string>
|
|
227
|
+
): boolean => {
|
|
228
|
+
const specifiers = getNodeSpecifiers(statement) ?? [];
|
|
229
|
+
|
|
230
|
+
for (const specifier of specifiers) {
|
|
231
|
+
const exported = getNodeExported(specifier);
|
|
232
|
+
const local = getNodeLocal(specifier);
|
|
233
|
+
const exportedName = identifierName(exported);
|
|
234
|
+
const localName = identifierName(local);
|
|
235
|
+
if (
|
|
236
|
+
exportedName &&
|
|
237
|
+
TOPO_EXPORT_NAMES.has(exportedName) &&
|
|
238
|
+
localName &&
|
|
239
|
+
topLevelTopoBindings.has(localName)
|
|
240
|
+
) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return false;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const moduleExportsTopo = (
|
|
249
|
+
ast: AstNode,
|
|
250
|
+
topoBindings: ImportedBindings
|
|
251
|
+
): boolean => {
|
|
252
|
+
const topLevelTopoBindings = collectTopLevelTopoBindings(ast, topoBindings);
|
|
253
|
+
const body = getNodeBodyStatements(ast);
|
|
254
|
+
|
|
255
|
+
for (const statement of body) {
|
|
256
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
257
|
+
const declaration = unwrapExportDeclaration(statement);
|
|
258
|
+
if (isTopoCall(declaration, topoBindings)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const defaultName = identifierName(declaration);
|
|
262
|
+
if (defaultName && topLevelTopoBindings.has(defaultName)) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (statement.type === 'ExportNamedDeclaration') {
|
|
268
|
+
const declaration = unwrapExportDeclaration(statement);
|
|
269
|
+
if (namedExportCarriesTopo(statement, topLevelTopoBindings)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (declaration.type !== 'VariableDeclaration') {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const declarations = getNodeDeclarations(declaration);
|
|
276
|
+
for (const item of declarations) {
|
|
277
|
+
const id = getNodeId(item);
|
|
278
|
+
const init = getNodeInit(item);
|
|
279
|
+
const name = declarationIdName(id);
|
|
280
|
+
if (
|
|
281
|
+
name &&
|
|
282
|
+
TOPO_EXPORT_NAMES.has(name) &&
|
|
283
|
+
isTopoCall(init, topoBindings)
|
|
284
|
+
) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const topLevelSurfaceOpen = (
|
|
295
|
+
statement: AstNode,
|
|
296
|
+
surfaceBindings: ImportedBindings
|
|
297
|
+
): AstNode | null => {
|
|
298
|
+
const declaration = unwrapExportDeclaration(statement);
|
|
299
|
+
if (declaration.type === 'ExpressionStatement') {
|
|
300
|
+
const expression = getNodeExpression(declaration);
|
|
301
|
+
const unwrapped = unwrapExpression(expression);
|
|
302
|
+
return isSurfaceOpenCall(unwrapped, surfaceBindings)
|
|
303
|
+
? (unwrapped ?? null)
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const unwrappedDeclaration = unwrapExpression(declaration);
|
|
308
|
+
if (isSurfaceOpenCall(unwrappedDeclaration, surfaceBindings)) {
|
|
309
|
+
return unwrappedDeclaration ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (declaration.type !== 'VariableDeclaration') {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const declarations = getNodeDeclarations(declaration);
|
|
317
|
+
for (const item of declarations) {
|
|
318
|
+
const init = getNodeInit(item);
|
|
319
|
+
const unwrapped = unwrapExpression(init);
|
|
320
|
+
if (isSurfaceOpenCall(unwrapped, surfaceBindings)) {
|
|
321
|
+
return unwrapped ?? null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const noTopLevelSurface: WardenRule = {
|
|
329
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
330
|
+
const ast = parse(filePath, sourceCode);
|
|
331
|
+
if (!ast) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const topoBindings = addFrameworkImportBindings(
|
|
336
|
+
ast,
|
|
337
|
+
TOPO_IMPORT_SOURCES,
|
|
338
|
+
new Set(['topo'])
|
|
339
|
+
);
|
|
340
|
+
if (!moduleExportsTopo(ast, topoBindings)) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const surfaceBindings = addFrameworkImportBindings(
|
|
344
|
+
ast,
|
|
345
|
+
SURFACE_IMPORT_SOURCES,
|
|
346
|
+
SURFACE_OPEN_CALLEE_NAMES
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
350
|
+
const body = getNodeBodyStatements(ast);
|
|
351
|
+
for (const statement of body) {
|
|
352
|
+
const surfaceOpen = topLevelSurfaceOpen(statement, surfaceBindings);
|
|
353
|
+
if (!surfaceOpen) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
diagnostics.push({
|
|
357
|
+
filePath,
|
|
358
|
+
line: offsetToLine(sourceCode, surfaceOpen.start),
|
|
359
|
+
message: diagnosticMessage,
|
|
360
|
+
rule: RULE_NAME,
|
|
361
|
+
severity: 'warn',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return diagnostics;
|
|
366
|
+
},
|
|
367
|
+
description:
|
|
368
|
+
'Coach topo export modules to keep surface-opening side effects out of module top level.',
|
|
369
|
+
name: RULE_NAME,
|
|
370
|
+
severity: 'warn',
|
|
371
|
+
};
|