@ontrails/warden 1.0.0-beta.39 → 1.0.0-beta.41
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 +125 -0
- package/README.md +5 -5
- package/package.json +10 -12
- package/src/cli.ts +49 -44
- package/src/command.ts +2 -2
- package/src/drift.ts +4 -3
- package/src/index.ts +3 -2
- package/src/project-context.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +12 -16
- package/src/rules/cli-command-route-coherence.ts +1 -1
- package/src/rules/composes-declarations.ts +15 -13
- package/src/rules/context-no-surface-types.ts +4 -3
- package/src/rules/dead-internal-trail.ts +3 -3
- package/src/rules/dead-public-trail.ts +3 -3
- package/src/rules/draft-file-marking.ts +4 -9
- package/src/rules/draft-visible-debt.ts +3 -8
- package/src/rules/duplicate-public-contract.ts +50 -5
- package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +2 -2
- package/src/rules/example-valid.ts +84 -84
- package/src/rules/fires-declarations.ts +27 -22
- package/src/rules/governed-symbol-residue.ts +339 -32
- package/src/rules/implementation-returns-result.ts +8 -8
- package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
- package/src/rules/incomplete-crud.ts +42 -40
- package/src/rules/index.ts +9 -6
- package/src/rules/intent-propagation.ts +3 -5
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/library-projection-coherence.ts +2 -2
- package/src/rules/metadata.ts +37 -15
- package/src/rules/missing-reconcile.ts +3 -4
- package/src/rules/missing-visibility.ts +2 -1
- package/src/rules/no-destructured-compose.ts +14 -12
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +2 -2
- package/src/rules/no-native-error-result.ts +2 -2
- package/src/rules/no-redundant-result-error-wrap.ts +10 -10
- package/src/rules/no-sync-result-assumption.ts +84 -78
- package/src/rules/no-throw-in-detour-recover.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +13 -7
- package/src/rules/no-top-level-surface.ts +2 -2
- package/src/rules/on-references-exist.ts +3 -3
- package/src/rules/orphaned-signal.ts +4 -5
- package/src/rules/owner-projection-parity.ts +2 -2
- package/src/rules/public-export-example-coverage.ts +10 -4
- package/src/rules/public-internal-deep-imports.ts +2 -3
- package/src/rules/read-intent-fires.ts +4 -3
- package/src/rules/reference-exists.ts +20 -21
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/resolved-import-boundary.ts +1 -1
- package/src/rules/resource-declarations.ts +21 -17
- package/src/rules/resource-exists.ts +4 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/retired-vocabulary.ts +242 -5
- 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/static-resource-accessor-preference.ts +15 -11
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/surface-trailhead-coherence.ts +2 -2
- package/src/rules/trail-fork-coaching.ts +27 -18
- package/src/rules/trail-versioning-source.ts +16 -12
- package/src/rules/trail-versioning-topo.ts +2 -2
- package/src/rules/trailhead-override-divergence.ts +2 -2
- package/src/rules/types.ts +7 -7
- package/src/rules/unmaterialized-activation-source.ts +1 -0
- package/src/rules/unreachable-detour-shadowing.ts +2 -2
- package/src/rules/valid-describe-refs.ts +2 -2
- package/src/rules/warden-export-symmetry.ts +3 -3
- package/src/rules/warden-rules-use-ast.ts +12 -17
- package/src/trails/activation-orphan.trail.ts +5 -5
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +6 -6
- package/src/trails/cli-command-route-coherence.trail.ts +2 -2
- package/src/trails/composes-declarations.trail.ts +1 -1
- package/src/trails/context-no-surface-types.trail.ts +1 -1
- package/src/trails/dead-internal-trail.trail.ts +2 -2
- package/src/trails/dead-public-trail.trail.ts +1 -1
- package/src/trails/deprecation-without-guidance.trail.ts +1 -1
- package/src/trails/duplicate-public-contract.trail.ts +3 -3
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/example-valid.trail.ts +3 -3
- package/src/trails/fires-declarations.trail.ts +1 -1
- package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
- package/src/trails/index.ts +3 -2
- package/src/trails/intent-propagation.trail.ts +2 -2
- package/src/trails/library-projection-coherence.trail.ts +2 -2
- package/src/trails/marker-schema-unsupported.trail.ts +1 -1
- package/src/trails/missing-visibility.trail.ts +1 -1
- package/src/trails/no-destructured-compose.trail.ts +5 -5
- package/src/trails/no-direct-implementation-call.trail.ts +1 -1
- package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/on-references-exist.trail.ts +1 -1
- package/src/trails/pending-force.trail.ts +1 -1
- package/src/trails/permit-governance.trail.ts +2 -2
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/public-output-schema.trail.ts +2 -2
- package/src/trails/public-union-output-discriminants.trail.ts +1 -1
- package/src/trails/read-intent-fires.trail.ts +1 -1
- package/src/trails/reference-exists.trail.ts +5 -5
- package/src/trails/resource-declarations.trail.ts +1 -1
- package/src/trails/resource-exists.trail.ts +1 -1
- package/src/trails/run.ts +4 -6
- package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
- package/src/trails/schema.ts +9 -9
- package/src/trails/signal-graph-coaching.trail.ts +2 -2
- package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
- package/src/trails/surface-overlay-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- package/src/trails/version-gap.trail.ts +1 -1
- package/src/trails/version-pinned-compose.trail.ts +1 -1
- package/src/trails/version-without-examples.trail.ts +1 -1
- package/src/trails/warden-rules-use-ast.trail.ts +2 -2
- package/src/trails/webhook-route-collision.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/src/ast.ts +0 -137
- package/src/rules/ast.ts +0 -4143
- package/src/rules/contour-ids.ts +0 -15
- package/src/trails/contour-exists.trail.ts +0 -21
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { getMemberExpression } from './source/stores.js';
|
|
1
2
|
import {
|
|
2
3
|
collectScopeFrameBindings,
|
|
3
|
-
|
|
4
|
+
findImplementationBodies,
|
|
4
5
|
findTrailDefinitions,
|
|
5
|
-
getMemberExpression,
|
|
6
|
-
getNodeArguments,
|
|
7
6
|
getNodeArgument,
|
|
7
|
+
getNodeArguments,
|
|
8
8
|
getNodeBodyNode,
|
|
9
9
|
getNodeCallee,
|
|
10
10
|
getNodeId,
|
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
parse,
|
|
20
20
|
walkWithParents,
|
|
21
21
|
walkWithScopes,
|
|
22
|
-
} from '
|
|
22
|
+
} from '@ontrails/source';
|
|
23
|
+
import type { AstNode, AstParentContext } from '@ontrails/source';
|
|
23
24
|
import {
|
|
24
25
|
collectAllResultHelperNames,
|
|
25
26
|
collectNamespaceHelperImports,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
trackScopedResultHelperDeclaration,
|
|
31
32
|
} from './implementation-returns-result.js';
|
|
32
33
|
import { isTestFile } from './scan.js';
|
|
33
|
-
import type { AstNode, AstParentContext } from './ast.js';
|
|
34
34
|
import type {
|
|
35
35
|
MutableScopedHelperMap,
|
|
36
36
|
NamespaceHelperMap,
|
|
@@ -344,12 +344,12 @@ export const noRedundantResultErrorWrap: WardenRule = {
|
|
|
344
344
|
const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
|
|
345
345
|
const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
|
|
346
346
|
const resultTypeNames = collectResultTypeNames(ast);
|
|
347
|
-
const
|
|
347
|
+
const implementationStarts = new Set<number>();
|
|
348
348
|
for (const def of findTrailDefinitions(ast)) {
|
|
349
|
-
for (const
|
|
350
|
-
|
|
349
|
+
for (const implementation of findImplementationBodies(def.config)) {
|
|
350
|
+
implementationStarts.add(implementation.start);
|
|
351
351
|
checkFunctionBody(
|
|
352
|
-
|
|
352
|
+
implementation,
|
|
353
353
|
`Trail "${def.id}"`,
|
|
354
354
|
filePath,
|
|
355
355
|
sourceCode,
|
|
@@ -361,7 +361,7 @@ export const noRedundantResultErrorWrap: WardenRule = {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
walkWithParents(ast, (node, context) => {
|
|
364
|
-
if (!isFunctionLike(node) ||
|
|
364
|
+
if (!isFunctionLike(node) || implementationStarts.has(node.start)) {
|
|
365
365
|
return;
|
|
366
366
|
}
|
|
367
367
|
checkFunctionBody(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resultAccessorNames } from '@ontrails/core';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
getNodeArgument,
|
|
5
4
|
getNodeAlternate,
|
|
5
|
+
getNodeArgument,
|
|
6
6
|
getNodeBodyNode,
|
|
7
7
|
getNodeBodyStatements,
|
|
8
8
|
getNodeConsequent,
|
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
getNodeRight,
|
|
25
25
|
getNodeValueNode,
|
|
26
26
|
identifierName,
|
|
27
|
-
|
|
27
|
+
isImplementationCall,
|
|
28
28
|
offsetToLine,
|
|
29
29
|
parse,
|
|
30
|
-
} from '
|
|
31
|
-
import type { AstNode } from '
|
|
30
|
+
} from '@ontrails/source';
|
|
31
|
+
import type { AstNode } from '@ontrails/source';
|
|
32
32
|
import { isFrameworkInternalFile, isTestFile } from './scan.js';
|
|
33
33
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
34
34
|
|
|
@@ -37,7 +37,7 @@ const RESULT_ACCESSOR_PROPERTIES: ReadonlySet<string> = new Set(
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
const MISSING_AWAIT_MESSAGE =
|
|
40
|
-
'Missing await: .
|
|
40
|
+
'Missing await: .implementation() returns Promise<Result> after normalization. Use `const result = await trail.implementation(input, ctx)`.';
|
|
41
41
|
|
|
42
42
|
const createMissingAwaitDiagnostic = (
|
|
43
43
|
filePath: string,
|
|
@@ -91,9 +91,9 @@ const buildParentMap = (ast: AstNode): WeakMap<AstNode, AstNode> => {
|
|
|
91
91
|
* Walk up the parent chain and return true when the expression is awaited
|
|
92
92
|
* before any result-accessing member access fires on it.
|
|
93
93
|
*
|
|
94
|
-
* `await x.
|
|
95
|
-
* `(await x.
|
|
96
|
-
* `x.
|
|
94
|
+
* `await x.implementation(...)` → awaited.
|
|
95
|
+
* `(await x.implementation(...)).isOk()` → awaited (await wraps before member access).
|
|
96
|
+
* `x.implementation(...).isOk()` → NOT awaited (member access on raw call).
|
|
97
97
|
*/
|
|
98
98
|
const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
99
99
|
'ParenthesizedExpression',
|
|
@@ -123,8 +123,8 @@ const skipParens = (
|
|
|
123
123
|
*
|
|
124
124
|
* Conservative: we only hop across a conditional when the node is one of
|
|
125
125
|
* its branches (not the `test` position). This lets us treat both
|
|
126
|
-
* `const r = cond ? x.
|
|
127
|
-
* `await (cond ? x.
|
|
126
|
+
* `const r = cond ? x.implementation(...) : fallback` and
|
|
127
|
+
* `await (cond ? x.implementation(...) : fallback)` correctly without misattributing
|
|
128
128
|
* calls used as conditions.
|
|
129
129
|
*/
|
|
130
130
|
const isBranchOfConditional = (outer: AstNode, parent: AstNode): boolean => {
|
|
@@ -137,9 +137,9 @@ const isBranchOfConditional = (outer: AstNode, parent: AstNode): boolean => {
|
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* Logical expressions (`&&`, `||`, `??`) carry the
|
|
141
|
-
* side. A `.
|
|
142
|
-
* declarator (e.g. `const r = cond && trail.
|
|
140
|
+
* Logical expressions (`&&`, `||`, `??`) carry the implementation result through either
|
|
141
|
+
* side. A `.implementation()` on either operand may be the value ultimately bound to a
|
|
142
|
+
* declarator (e.g. `const r = cond && trail.implementation(...)`), so we treat both
|
|
143
143
|
* operands as carriers.
|
|
144
144
|
*/
|
|
145
145
|
const isOperandOfLogical = (outer: AstNode, parent: AstNode): boolean => {
|
|
@@ -175,9 +175,9 @@ const isAwaited = (
|
|
|
175
175
|
node: AstNode,
|
|
176
176
|
parents: WeakMap<AstNode, AstNode>
|
|
177
177
|
): boolean => {
|
|
178
|
-
// Walk up through parens and any conditional whose branch is the
|
|
179
|
-
// call. `await (c ? x.
|
|
180
|
-
// whole, so the
|
|
178
|
+
// Walk up through parens and any conditional whose branch is the implementation
|
|
179
|
+
// call. `await (c ? x.implementation(...) : fallback)` awaits the conditional as a
|
|
180
|
+
// whole, so the implementation call in a branch is effectively awaited.
|
|
181
181
|
const outer = skipParensAndBranchConditionals(node, parents);
|
|
182
182
|
return parents.get(outer)?.type === 'AwaitExpression';
|
|
183
183
|
};
|
|
@@ -197,19 +197,19 @@ const memberPropertyName = (node: AstNode): string | null => {
|
|
|
197
197
|
};
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Check if the
|
|
201
|
-
* (e.g. `foo.
|
|
200
|
+
* Check if the implementation call is directly consumed by a result accessor
|
|
201
|
+
* (e.g. `foo.implementation(...).isOk()` or `foo.implementation(...).value`).
|
|
202
202
|
*/
|
|
203
203
|
const hasDirectResultAccess = (
|
|
204
|
-
|
|
204
|
+
implementationCall: AstNode,
|
|
205
205
|
parents: WeakMap<AstNode, AstNode>
|
|
206
206
|
): boolean => {
|
|
207
207
|
// Unwrap wrapping parentheses, conditional branches, and logical-operator
|
|
208
|
-
// operands so `(x.
|
|
209
|
-
// `(cond ? x.
|
|
210
|
-
// `(cond && x.
|
|
211
|
-
// bare `x.
|
|
212
|
-
const outer = skipParensAndBranchConditionals(
|
|
208
|
+
// operands so `(x.implementation(...)).isOk()`,
|
|
209
|
+
// `(cond ? x.implementation(...) : fb).isOk()`, and
|
|
210
|
+
// `(cond && x.implementation(...)).isOk()` are all detected the same way as the
|
|
211
|
+
// bare `x.implementation(...).isOk()` shape.
|
|
212
|
+
const outer = skipParensAndBranchConditionals(implementationCall, parents);
|
|
213
213
|
const parent = parents.get(outer);
|
|
214
214
|
if (!parent) {
|
|
215
215
|
return false;
|
|
@@ -219,15 +219,15 @@ const hasDirectResultAccess = (
|
|
|
219
219
|
};
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* If the
|
|
222
|
+
* If the implementation call is the init of a VariableDeclarator (directly, through
|
|
223
223
|
* parens, or as a branch of a ConditionalExpression init), return the bound
|
|
224
224
|
* identifier name. Otherwise null.
|
|
225
225
|
*/
|
|
226
226
|
const extractAssignedBinding = (
|
|
227
|
-
|
|
227
|
+
implementationCall: AstNode,
|
|
228
228
|
parents: WeakMap<AstNode, AstNode>
|
|
229
229
|
): string | null => {
|
|
230
|
-
const outer = skipParensAndBranchConditionals(
|
|
230
|
+
const outer = skipParensAndBranchConditionals(implementationCall, parents);
|
|
231
231
|
const parent = parents.get(outer);
|
|
232
232
|
if (!parent || parent.type !== 'VariableDeclarator') {
|
|
233
233
|
return null;
|
|
@@ -539,7 +539,7 @@ const collectBlockStatementBindings = (scope: AstNode): Set<string> => {
|
|
|
539
539
|
// so `var` declarations inside them do not escape into the enclosing class
|
|
540
540
|
// or function scope. `collectHoistedVarBindings` correctly refuses to compose
|
|
541
541
|
// a `StaticBlock` boundary from the outside, which means nothing else will
|
|
542
|
-
// register these bindings. Hoist them here so `var result = trail.
|
|
542
|
+
// register these bindings. Hoist them here so `var result = trail.implementation(...)`
|
|
543
543
|
// inside a `static { ... }` block is tracked against the block itself.
|
|
544
544
|
if (scope.type === 'StaticBlock') {
|
|
545
545
|
// `collectHoistedVarBindings` is called with the StaticBlock as the root,
|
|
@@ -586,7 +586,7 @@ interface ScopeFrame {
|
|
|
586
586
|
* For function frames: names that came from parameters (not hoisted `var`s).
|
|
587
587
|
* A `var` declaration with the same name as a parameter is redundant in JS —
|
|
588
588
|
* the parameter is the real binding. We track params separately so we don't
|
|
589
|
-
* register a pending `.
|
|
589
|
+
* register a pending `.implementation()` binding that is actually shadowed by a param.
|
|
590
590
|
*/
|
|
591
591
|
readonly paramBindings?: Set<string>;
|
|
592
592
|
}
|
|
@@ -647,7 +647,7 @@ interface AnalyzeState {
|
|
|
647
647
|
readonly diagnostics: WardenDiagnostic[];
|
|
648
648
|
readonly sourceCode: string;
|
|
649
649
|
readonly filePath: string;
|
|
650
|
-
/** Pending `.
|
|
650
|
+
/** Pending `.implementation()` bindings seen so far, keyed by scope id + name. */
|
|
651
651
|
readonly pendingByScopeAndName: Map<string, PendingBinding>;
|
|
652
652
|
readonly scopeStack: ScopeFrame[];
|
|
653
653
|
readonly reportedAt: Set<number>;
|
|
@@ -683,15 +683,15 @@ const resolveNearestScope = (
|
|
|
683
683
|
};
|
|
684
684
|
|
|
685
685
|
/**
|
|
686
|
-
* Resolve the
|
|
686
|
+
* Resolve the implementation call to a `{ name, declarator }` pair when it is the init
|
|
687
687
|
* of a `VariableDeclarator` (directly, through parens, or as a branch of a
|
|
688
688
|
* `ConditionalExpression` init). Returns null otherwise.
|
|
689
689
|
*/
|
|
690
|
-
const
|
|
691
|
-
|
|
690
|
+
const resolveImplementationBinding = (
|
|
691
|
+
implementationCall: AstNode,
|
|
692
692
|
parents: WeakMap<AstNode, AstNode>
|
|
693
693
|
): { readonly name: string; readonly declarator: AstNode } | null => {
|
|
694
|
-
const name = extractAssignedBinding(
|
|
694
|
+
const name = extractAssignedBinding(implementationCall, parents);
|
|
695
695
|
if (!name) {
|
|
696
696
|
return null;
|
|
697
697
|
}
|
|
@@ -699,22 +699,22 @@ const resolveBlazeBinding = (
|
|
|
699
699
|
// conditionals so the stored declaration node points at the
|
|
700
700
|
// `VariableDeclarator`, not at an intermediate `ParenthesizedExpression`
|
|
701
701
|
// or `ConditionalExpression`.
|
|
702
|
-
const outer = skipParensAndBranchConditionals(
|
|
702
|
+
const outer = skipParensAndBranchConditionals(implementationCall, parents);
|
|
703
703
|
const declarator = parents.get(outer);
|
|
704
704
|
return declarator ? { declarator, name } : null;
|
|
705
705
|
};
|
|
706
706
|
|
|
707
707
|
/**
|
|
708
|
-
* Resolve the
|
|
708
|
+
* Resolve the implementation call to a `{ name, assignment }` pair when it is the RHS
|
|
709
709
|
* of a plain `=` `AssignmentExpression` with an `Identifier` LHS (directly,
|
|
710
710
|
* through parens, or as a branch of a conditional/logical expression).
|
|
711
711
|
*
|
|
712
712
|
* Covers patterns like:
|
|
713
713
|
* let result;
|
|
714
|
-
* result = trail.
|
|
714
|
+
* result = trail.implementation(input, ctx);
|
|
715
715
|
* result.isOk();
|
|
716
716
|
*
|
|
717
|
-
* Member-expression LHS (`obj.result =
|
|
717
|
+
* Member-expression LHS (`obj.result = implementation(...)`) is intentionally skipped —
|
|
718
718
|
* those are property writes, not bare bindings we can track by name.
|
|
719
719
|
*/
|
|
720
720
|
const extractPlainIdentifierAssignmentName = (
|
|
@@ -726,7 +726,7 @@ const extractPlainIdentifierAssignmentName = (
|
|
|
726
726
|
const operator = getNodeOperator(parent);
|
|
727
727
|
const left = getNodeLeft(parent);
|
|
728
728
|
// Only plain `=` assignments to a bare identifier. Member-expression LHS
|
|
729
|
-
// (`obj.result =
|
|
729
|
+
// (`obj.result = implementation(...)`) is a property write, not a bare binding we
|
|
730
730
|
// can track by name.
|
|
731
731
|
if (operator !== '=' || !left || left.type !== 'Identifier') {
|
|
732
732
|
return null;
|
|
@@ -734,11 +734,11 @@ const extractPlainIdentifierAssignmentName = (
|
|
|
734
734
|
return identifierName(left);
|
|
735
735
|
};
|
|
736
736
|
|
|
737
|
-
const
|
|
738
|
-
|
|
737
|
+
const resolveImplementationAssignment = (
|
|
738
|
+
implementationCall: AstNode,
|
|
739
739
|
parents: WeakMap<AstNode, AstNode>
|
|
740
740
|
): { readonly name: string; readonly assignment: AstNode } | null => {
|
|
741
|
-
const outer = skipParensAndBranchConditionals(
|
|
741
|
+
const outer = skipParensAndBranchConditionals(implementationCall, parents);
|
|
742
742
|
const parent = parents.get(outer);
|
|
743
743
|
const name = extractPlainIdentifierAssignmentName(parent);
|
|
744
744
|
return name && parent ? { assignment: parent, name } : null;
|
|
@@ -767,7 +767,7 @@ const isVarDeclaratorOfParamName = (
|
|
|
767
767
|
* that name is a function parameter, the assignment re-initializes the
|
|
768
768
|
* parameter's slot in the VariableEnvironment, just like `var <name> = ...`.
|
|
769
769
|
* Compound assignments (`+=`, `??=`, etc.) are excluded because they do not
|
|
770
|
-
* unconditionally replace the slot with the
|
|
770
|
+
* unconditionally replace the slot with the implementation result.
|
|
771
771
|
*/
|
|
772
772
|
const isAssignmentToParamName = (node: AstNode): boolean => {
|
|
773
773
|
if (node.type !== 'AssignmentExpression') {
|
|
@@ -779,13 +779,16 @@ const isAssignmentToParamName = (node: AstNode): boolean => {
|
|
|
779
779
|
};
|
|
780
780
|
|
|
781
781
|
const recordPendingBinding = (
|
|
782
|
-
|
|
782
|
+
implementationCall: AstNode,
|
|
783
783
|
state: AnalyzeState
|
|
784
784
|
): void => {
|
|
785
785
|
const binding =
|
|
786
|
-
|
|
786
|
+
resolveImplementationBinding(implementationCall, state.parents) ??
|
|
787
787
|
(() => {
|
|
788
|
-
const asn =
|
|
788
|
+
const asn = resolveImplementationAssignment(
|
|
789
|
+
implementationCall,
|
|
790
|
+
state.parents
|
|
791
|
+
);
|
|
789
792
|
return asn ? { declarator: asn.assignment, name: asn.name } : null;
|
|
790
793
|
})();
|
|
791
794
|
if (!binding) {
|
|
@@ -802,21 +805,21 @@ const recordPendingBinding = (
|
|
|
802
805
|
}
|
|
803
806
|
// If the name resolves to a function parameter, the `var` that visually
|
|
804
807
|
// appears to declare it is redundant — the parameter is the real binding,
|
|
805
|
-
// and parameters are not pending `.
|
|
808
|
+
// and parameters are not pending `.implementation()` results.
|
|
806
809
|
//
|
|
807
|
-
// Carve-out: a `var <name> =
|
|
810
|
+
// Carve-out: a `var <name> = implementation(...)` *initializer* inside the same
|
|
808
811
|
// function body legitimately re-binds the parameter at that point. `var`
|
|
809
812
|
// and parameters share the function's VariableEnvironment, so the `var`
|
|
810
813
|
// writes to the existing parameter slot and the subsequent use resolves
|
|
811
|
-
// to the freshly-assigned `.
|
|
814
|
+
// to the freshly-assigned `.implementation()` result. Treat that as a pending
|
|
812
815
|
// binding.
|
|
813
816
|
//
|
|
814
|
-
// The same logic applies to a bare `result =
|
|
817
|
+
// The same logic applies to a bare `result = implementation(...)` assignment: it
|
|
815
818
|
// writes to the parameter's existing slot in the same VariableEnvironment,
|
|
816
|
-
// so the subsequent `result.isOk()` observes the
|
|
819
|
+
// so the subsequent `result.isOk()` observes the implementation result. Only
|
|
817
820
|
// compound assignments (`+=`, `??=`, etc.) and member-expression LHS fall
|
|
818
821
|
// through the param-shadow suppression, because they do not
|
|
819
|
-
// unconditionally replace the parameter slot with the
|
|
822
|
+
// unconditionally replace the parameter slot with the implementation result.
|
|
820
823
|
if (
|
|
821
824
|
owningFrame.paramBindings?.has(name) &&
|
|
822
825
|
!isVarDeclaratorOfParamName(declarator, state.parents) &&
|
|
@@ -833,12 +836,12 @@ const recordPendingBinding = (
|
|
|
833
836
|
|
|
834
837
|
/**
|
|
835
838
|
* True when `expr`, descended through wrapping parens, conditional branches,
|
|
836
|
-
* and logical-operator operands, contains a `.
|
|
839
|
+
* and logical-operator operands, contains a `.implementation()` call that would be
|
|
837
840
|
* registered by `recordPendingBinding` for this assignment.
|
|
838
841
|
*
|
|
839
842
|
* This mirrors the *upward* carrier walk done by
|
|
840
|
-
* `skipParensAndBranchConditionals` — if a
|
|
841
|
-
* carrier path descending from `expr`, then visiting that
|
|
843
|
+
* `skipParensAndBranchConditionals` — if a implementation call is anywhere along a
|
|
844
|
+
* carrier path descending from `expr`, then visiting that implementation call will
|
|
842
845
|
* re-register the pending binding, so we must not clear it on the way in.
|
|
843
846
|
*/
|
|
844
847
|
type CarrierChildExtractor = (
|
|
@@ -862,23 +865,23 @@ const unwrapTransparentWrapper = (expr: AstNode): AstNode | undefined =>
|
|
|
862
865
|
|
|
863
866
|
// biome-ignore lint/style/useConst: hoisted for recursive call
|
|
864
867
|
// eslint-disable-next-line func-style
|
|
865
|
-
function
|
|
868
|
+
function rhsCarriesImplementationReinit(expr: AstNode | undefined): boolean {
|
|
866
869
|
if (!expr) {
|
|
867
870
|
return false;
|
|
868
871
|
}
|
|
869
872
|
if (TRANSPARENT_WRAPPER_TYPES.has(expr.type)) {
|
|
870
|
-
return
|
|
873
|
+
return rhsCarriesImplementationReinit(unwrapTransparentWrapper(expr));
|
|
871
874
|
}
|
|
872
875
|
const extractor = CARRIER_CHILDREN[expr.type];
|
|
873
876
|
if (extractor) {
|
|
874
|
-
return extractor(expr).some(
|
|
877
|
+
return extractor(expr).some(rhsCarriesImplementationReinit);
|
|
875
878
|
}
|
|
876
|
-
return
|
|
879
|
+
return isImplementationCall(expr);
|
|
877
880
|
}
|
|
878
881
|
|
|
879
882
|
/**
|
|
880
883
|
* Nullish/falsy-skip compound assignments (`??=`, `||=`) only write to the slot
|
|
881
|
-
* when the LHS is nullish or falsy. A pending `.
|
|
884
|
+
* when the LHS is nullish or falsy. A pending `.implementation()` binding holds a
|
|
882
885
|
* truthy `Promise<Result>`, so the RHS never runs and the pending binding must
|
|
883
886
|
* survive them.
|
|
884
887
|
*
|
|
@@ -924,10 +927,10 @@ const resolvePendingKeyFor = (
|
|
|
924
927
|
|
|
925
928
|
/**
|
|
926
929
|
* Handle a plain `=` assignment (or clearing compound assignment) to a bare
|
|
927
|
-
* identifier whose name currently has a pending `.
|
|
930
|
+
* identifier whose name currently has a pending `.implementation()` binding in scope.
|
|
928
931
|
*
|
|
929
|
-
* A plain `=` whose RHS carries another
|
|
930
|
-
* alone — `recordPendingBinding` will re-register it when the
|
|
932
|
+
* A plain `=` whose RHS carries another implementation call leaves the pending entry
|
|
933
|
+
* alone — `recordPendingBinding` will re-register it when the implementation call
|
|
931
934
|
* itself is visited. Otherwise, clear the pending entry: the identifier has
|
|
932
935
|
* been overwritten with a non-Result value, so the original
|
|
933
936
|
* `result.isOk()`-style diagnostic no longer applies.
|
|
@@ -951,11 +954,14 @@ const handleAssignmentReassignment = (
|
|
|
951
954
|
if (!key) {
|
|
952
955
|
return;
|
|
953
956
|
}
|
|
954
|
-
// Plain `=` with a
|
|
955
|
-
// `recordPendingBinding` when the
|
|
957
|
+
// Plain `=` with a implementation-carrying RHS will re-register via
|
|
958
|
+
// `recordPendingBinding` when the implementation call itself is visited. Other
|
|
956
959
|
// compound operators (`+=`, `-=`, `*=`, etc.) produce a primitive value
|
|
957
960
|
// from the existing slot, so they always clear.
|
|
958
|
-
if (
|
|
961
|
+
if (
|
|
962
|
+
assignment.operator === '=' &&
|
|
963
|
+
rhsCarriesImplementationReinit(assignment.right)
|
|
964
|
+
) {
|
|
959
965
|
return;
|
|
960
966
|
}
|
|
961
967
|
state.pendingByScopeAndName.delete(key);
|
|
@@ -1005,12 +1011,12 @@ const checkPendingAccess = (node: AstNode, state: AnalyzeState): void => {
|
|
|
1005
1011
|
};
|
|
1006
1012
|
|
|
1007
1013
|
/**
|
|
1008
|
-
* If the
|
|
1014
|
+
* If the implementation call is the init of a VariableDeclarator whose id is an
|
|
1009
1015
|
* ObjectPattern that destructures any known Result accessor property,
|
|
1010
1016
|
* return the declarator node. Otherwise null.
|
|
1011
1017
|
*
|
|
1012
1018
|
* Catches the core missing-await shape when written as destructuring:
|
|
1013
|
-
* `const { isOk } = entityShow.
|
|
1019
|
+
* `const { isOk } = entityShow.implementation(input, ctx)` — no await, immediate
|
|
1014
1020
|
* access to a Result accessor, should fire.
|
|
1015
1021
|
*/
|
|
1016
1022
|
const propertyDestructuresResultAccessor = (prop: AstNode): boolean => {
|
|
@@ -1027,14 +1033,14 @@ const objectPatternHasResultAccessorKey = (pattern: AstNode): boolean => {
|
|
|
1027
1033
|
};
|
|
1028
1034
|
|
|
1029
1035
|
const getDestructuredResultAccessorDeclarator = (
|
|
1030
|
-
|
|
1036
|
+
implementationCall: AstNode,
|
|
1031
1037
|
parents: WeakMap<AstNode, AstNode>
|
|
1032
1038
|
): AstNode | null => {
|
|
1033
1039
|
// Unwrap any wrapping parentheses and branch-position conditionals so
|
|
1034
|
-
// `const { isOk } = (trail.
|
|
1035
|
-
// `const { isOk } = cond ? trail.
|
|
1036
|
-
// `const { isOk } = trail.
|
|
1037
|
-
const outer = skipParensAndBranchConditionals(
|
|
1040
|
+
// `const { isOk } = (trail.implementation(...));` and
|
|
1041
|
+
// `const { isOk } = cond ? trail.implementation(...) : fallback;` are treated as
|
|
1042
|
+
// `const { isOk } = trail.implementation(...);`.
|
|
1043
|
+
const outer = skipParensAndBranchConditionals(implementationCall, parents);
|
|
1038
1044
|
const parent = parents.get(outer);
|
|
1039
1045
|
if (!parent || parent.type !== 'VariableDeclarator') {
|
|
1040
1046
|
return null;
|
|
@@ -1046,8 +1052,8 @@ const getDestructuredResultAccessorDeclarator = (
|
|
|
1046
1052
|
return objectPatternHasResultAccessorKey(id) ? parent : null;
|
|
1047
1053
|
};
|
|
1048
1054
|
|
|
1049
|
-
const
|
|
1050
|
-
if (!
|
|
1055
|
+
const visitImplementationCall = (node: AstNode, state: AnalyzeState): void => {
|
|
1056
|
+
if (!isImplementationCall(node) || isAwaited(node, state.parents)) {
|
|
1051
1057
|
return;
|
|
1052
1058
|
}
|
|
1053
1059
|
if (hasDirectResultAccess(node, state.parents)) {
|
|
@@ -1066,7 +1072,7 @@ const visitBlazeCall = (node: AstNode, state: AnalyzeState): void => {
|
|
|
1066
1072
|
};
|
|
1067
1073
|
|
|
1068
1074
|
const visitNode = (node: AstNode, state: AnalyzeState): void => {
|
|
1069
|
-
|
|
1075
|
+
visitImplementationCall(node, state);
|
|
1070
1076
|
checkPendingAccess(node, state);
|
|
1071
1077
|
};
|
|
1072
1078
|
|
|
@@ -1173,7 +1179,7 @@ const analyze = (
|
|
|
1173
1179
|
};
|
|
1174
1180
|
|
|
1175
1181
|
/**
|
|
1176
|
-
* Flags code that assumes `.
|
|
1182
|
+
* Flags code that assumes `.implementation()` returns a synchronous result.
|
|
1177
1183
|
*/
|
|
1178
1184
|
export const noSyncResultAssumption: WardenRule = {
|
|
1179
1185
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
@@ -1187,7 +1193,7 @@ export const noSyncResultAssumption: WardenRule = {
|
|
|
1187
1193
|
return analyze(ast, sourceCode, filePath);
|
|
1188
1194
|
},
|
|
1189
1195
|
description:
|
|
1190
|
-
'Disallow treating .
|
|
1196
|
+
'Disallow treating .implementation() as synchronous after normalization. Always await the returned Promise<Result>.',
|
|
1191
1197
|
name: 'no-sync-result-assumption',
|
|
1192
1198
|
severity: 'error',
|
|
1193
1199
|
};
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
parse,
|
|
7
7
|
walk,
|
|
8
8
|
walkScope,
|
|
9
|
-
} from '
|
|
10
|
-
import type { AstNode } from '
|
|
9
|
+
} from '@ontrails/source';
|
|
10
|
+
import type { AstNode } from '@ontrails/source';
|
|
11
11
|
import { isTestFile } from './scan.js';
|
|
12
12
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
13
13
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Finds `throw` statements inside `
|
|
2
|
+
* Finds `throw` statements inside `implementation:` function bodies.
|
|
3
3
|
*
|
|
4
4
|
* Uses scope-aware AST walking so throws inside nested callbacks
|
|
5
5
|
* (e.g. `.map()`, `.filter()`, inner helpers) are not attributed to
|
|
6
|
-
* the
|
|
7
|
-
* to be avoided — only throws in the
|
|
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.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findImplementationBodies,
|
|
12
|
+
offsetToLine,
|
|
13
|
+
parse,
|
|
14
|
+
walkScope,
|
|
15
|
+
} from '@ontrails/source';
|
|
11
16
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
12
17
|
|
|
13
18
|
export const noThrowInImplementation: WardenRule = {
|
|
@@ -19,13 +24,14 @@ export const noThrowInImplementation: WardenRule = {
|
|
|
19
24
|
|
|
20
25
|
const diagnostics: WardenDiagnostic[] = [];
|
|
21
26
|
|
|
22
|
-
for (const body of
|
|
27
|
+
for (const body of findImplementationBodies(ast)) {
|
|
23
28
|
walkScope(body, (node) => {
|
|
24
29
|
if (node.type === 'ThrowStatement') {
|
|
25
30
|
diagnostics.push({
|
|
26
31
|
filePath,
|
|
27
32
|
line: offsetToLine(sourceCode, node.start),
|
|
28
|
-
message:
|
|
33
|
+
message:
|
|
34
|
+
'Do not throw inside the implementation. Use Result.err() instead.',
|
|
29
35
|
rule: 'no-throw-in-implementation',
|
|
30
36
|
severity: 'error',
|
|
31
37
|
});
|
|
@@ -36,7 +42,7 @@ export const noThrowInImplementation: WardenRule = {
|
|
|
36
42
|
return diagnostics;
|
|
37
43
|
},
|
|
38
44
|
description:
|
|
39
|
-
'Disallow throw statements inside
|
|
45
|
+
'Disallow throw statements inside implementation bodies. Use Result.err() instead.',
|
|
40
46
|
name: 'no-throw-in-implementation',
|
|
41
47
|
severity: 'error',
|
|
42
48
|
};
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
isStringLiteral,
|
|
22
22
|
offsetToLine,
|
|
23
23
|
parse,
|
|
24
|
-
} from '
|
|
25
|
-
import type { AstNode } from '
|
|
24
|
+
} from '@ontrails/source';
|
|
25
|
+
import type { AstNode } from '@ontrails/source';
|
|
26
26
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
27
27
|
|
|
28
28
|
const RULE_NAME = 'no-top-level-surface';
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { isDraftId } from '@ontrails/core';
|
|
11
11
|
|
|
12
|
+
import { collectSignalDefinitionIds } from './source/signals.js';
|
|
12
13
|
import {
|
|
13
|
-
collectSignalDefinitionIds,
|
|
14
14
|
deriveConstString,
|
|
15
15
|
findConfigProperty,
|
|
16
16
|
findTrailDefinitions,
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
isStringLiteral,
|
|
20
20
|
offsetToLine,
|
|
21
21
|
parse,
|
|
22
|
-
} from '
|
|
23
|
-
import type { AstNode } from '
|
|
22
|
+
} from '@ontrails/source';
|
|
23
|
+
import type { AstNode } from '@ontrails/source';
|
|
24
24
|
import { isTestFile } from './scan.js';
|
|
25
25
|
import type {
|
|
26
26
|
ProjectAwareWardenRule,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { collectOnTargetSignalIds } from './source/signals.js';
|
|
1
2
|
import {
|
|
2
3
|
collectCrudTableIds,
|
|
3
|
-
collectOnTargetSignalIds,
|
|
4
4
|
findStoreTableDefinitions,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import type { AstNode } from './ast.js';
|
|
5
|
+
} from './source/stores.js';
|
|
6
|
+
import { offsetToLine, parse } from '@ontrails/source';
|
|
7
|
+
import type { AstNode } from '@ontrails/source';
|
|
9
8
|
import { isTestFile } from './scan.js';
|
|
10
9
|
import type {
|
|
11
10
|
ProjectAwareWardenRule,
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
offsetToLine,
|
|
15
15
|
parse,
|
|
16
16
|
walk,
|
|
17
|
-
} from '
|
|
18
|
-
import type { AstNode } from '
|
|
17
|
+
} from '@ontrails/source';
|
|
18
|
+
import type { AstNode } from '@ontrails/source';
|
|
19
19
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
20
20
|
|
|
21
21
|
const RULE_NAME = 'owner-projection-parity';
|
|
@@ -34,8 +34,8 @@ import {
|
|
|
34
34
|
getNodeBodyStatements,
|
|
35
35
|
getNodeDeclaration,
|
|
36
36
|
getNodeDeclarations,
|
|
37
|
-
getNodeExportKind,
|
|
38
37
|
getNodeExported,
|
|
38
|
+
getNodeExportKind,
|
|
39
39
|
getNodeId,
|
|
40
40
|
getNodeLocal,
|
|
41
41
|
getNodeName,
|
|
@@ -44,8 +44,8 @@ import {
|
|
|
44
44
|
getNodeValue,
|
|
45
45
|
offsetToLine,
|
|
46
46
|
parse,
|
|
47
|
-
} from '
|
|
48
|
-
import type { AstNode } from '
|
|
47
|
+
} from '@ontrails/source';
|
|
48
|
+
import type { AstNode } from '@ontrails/source';
|
|
49
49
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
50
50
|
|
|
51
51
|
const RULE_NAME = 'public-export-example-coverage';
|
|
@@ -115,7 +115,13 @@ export const PUBLIC_API_EXAMPLE_TARGETS: readonly PublicApiPackageTarget[] = [
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
indexPath: 'adapters/cloudflare/src/index.ts',
|
|
118
|
-
minimumExports: [
|
|
118
|
+
minimumExports: [
|
|
119
|
+
'createWorkersHandler',
|
|
120
|
+
'cloudflareKv',
|
|
121
|
+
'cloudflareD1',
|
|
122
|
+
'cloudflareQueue',
|
|
123
|
+
'cloudflareR2',
|
|
124
|
+
],
|
|
119
125
|
packageName: '@ontrails/cloudflare',
|
|
120
126
|
},
|
|
121
127
|
] as const;
|
|
@@ -2,15 +2,14 @@ import { existsSync, realpathSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, sep } from 'node:path';
|
|
3
3
|
import { matchesPathGlob } from '@ontrails/core';
|
|
4
4
|
|
|
5
|
+
import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
|
|
5
6
|
import {
|
|
6
7
|
extractStringLiteral,
|
|
7
8
|
getNodeSource,
|
|
8
|
-
hasIgnoreCommentOnLine,
|
|
9
9
|
offsetToLine,
|
|
10
10
|
parse,
|
|
11
|
-
splitSourceLines,
|
|
12
11
|
walk,
|
|
13
|
-
} from '
|
|
12
|
+
} from '@ontrails/source';
|
|
14
13
|
import type {
|
|
15
14
|
ProjectAwareWardenRule,
|
|
16
15
|
ProjectContext,
|