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