@ontrails/warden 1.0.0-beta.23 → 1.0.0-beta.29
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 +113 -0
- package/README.md +35 -1
- package/package.json +10 -9
- package/src/ast.ts +109 -0
- package/src/cli.ts +44 -4
- package/src/command.ts +24 -34
- package/src/drift.ts +24 -8
- package/src/index.ts +9 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +8 -7
- package/src/rules/ast.ts +837 -6
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +210 -77
- package/src/rules/context-no-surface-types.ts +15 -12
- package/src/rules/contour-exists.ts +7 -7
- package/src/rules/dead-internal-trail.ts +11 -4
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +5 -3
- package/src/rules/example-valid.ts +6 -12
- package/src/rules/fires-declarations.ts +41 -64
- package/src/rules/implementation-returns-result.ts +208 -76
- package/src/rules/incomplete-crud.ts +20 -19
- package/src/rules/index.ts +15 -0
- package/src/rules/layer-field-name-drift.ts +12 -6
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +102 -1
- package/src/rules/no-destructured-compose.ts +16 -14
- package/src/rules/no-native-error-result.ts +15 -8
- package/src/rules/no-redundant-result-error-wrap.ts +82 -29
- package/src/rules/no-sync-result-assumption.ts +70 -68
- package/src/rules/no-top-level-surface.ts +46 -64
- package/src/rules/on-references-exist.ts +1 -1
- package/src/rules/owner-projection-parity.ts +10 -13
- package/src/rules/public-export-example-coverage.ts +29 -20
- package/src/rules/public-internal-deep-imports.ts +2 -2
- package/src/rules/read-intent-fires.ts +8 -8
- package/src/rules/registry-names.ts +10 -0
- package/src/rules/resource-declarations.ts +20 -31
- package/src/rules/resource-exists.ts +2 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/static-resource-accessor-preference.ts +26 -34
- package/src/rules/surface-facet-coherence.ts +21 -29
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +56 -78
- package/src/rules/types.ts +2 -0
- package/src/rules/unreachable-detour-shadowing.ts +16 -21
- package/src/rules/valid-describe-refs.ts +14 -12
- package/src/rules/warden-export-symmetry.ts +42 -35
- package/src/rules/warden-rules-use-ast.ts +168 -50
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +1 -0
- package/src/trails/index.ts +5 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/schema.ts +4 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/warden-rules-use-ast.trail.ts +19 -0
- package/src/trails/wrap-rule.ts +1 -0
|
@@ -3,10 +3,21 @@ import {
|
|
|
3
3
|
findBlazeBodies,
|
|
4
4
|
findTrailDefinitions,
|
|
5
5
|
getMemberExpression,
|
|
6
|
+
getNodeArguments,
|
|
7
|
+
getNodeArgument,
|
|
8
|
+
getNodeBodyNode,
|
|
9
|
+
getNodeCallee,
|
|
10
|
+
getNodeId,
|
|
11
|
+
getNodeInit,
|
|
12
|
+
getNodeLeft,
|
|
13
|
+
getNodeOperator,
|
|
14
|
+
getNodeProperty,
|
|
15
|
+
getNodeRight,
|
|
6
16
|
identifierName,
|
|
7
17
|
isMemberAccessNonComputed,
|
|
8
18
|
offsetToLine,
|
|
9
19
|
parse,
|
|
20
|
+
walkWithParents,
|
|
10
21
|
walkWithScopes,
|
|
11
22
|
} from './ast.js';
|
|
12
23
|
import {
|
|
@@ -19,7 +30,7 @@ import {
|
|
|
19
30
|
trackScopedResultHelperDeclaration,
|
|
20
31
|
} from './implementation-returns-result.js';
|
|
21
32
|
import { isTestFile } from './scan.js';
|
|
22
|
-
import type { AstNode } from './ast.js';
|
|
33
|
+
import type { AstNode, AstParentContext } from './ast.js';
|
|
23
34
|
import type {
|
|
24
35
|
MutableScopedHelperMap,
|
|
25
36
|
NamespaceHelperMap,
|
|
@@ -33,7 +44,7 @@ const getStaticMemberName = (node: AstNode | undefined): string | null => {
|
|
|
33
44
|
if (!node || !isMemberAccessNonComputed(node)) {
|
|
34
45
|
return null;
|
|
35
46
|
}
|
|
36
|
-
return identifierName((node
|
|
47
|
+
return identifierName(getNodeProperty(node));
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
const isResultObject = (node: AstNode | undefined): boolean => {
|
|
@@ -50,7 +61,7 @@ const isResultErrCall = (node: AstNode): boolean => {
|
|
|
50
61
|
if (node.type !== 'CallExpression') {
|
|
51
62
|
return false;
|
|
52
63
|
}
|
|
53
|
-
const
|
|
64
|
+
const callee = getNodeCallee(node);
|
|
54
65
|
const member = getMemberExpression(callee);
|
|
55
66
|
if (!member || getStaticMemberName(callee) !== 'err') {
|
|
56
67
|
return false;
|
|
@@ -59,8 +70,7 @@ const isResultErrCall = (node: AstNode): boolean => {
|
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
const getSingleArgument = (node: AstNode): AstNode | null => {
|
|
62
|
-
const args = (node
|
|
63
|
-
.arguments;
|
|
73
|
+
const args = getNodeArguments(node);
|
|
64
74
|
return args?.length === 1 ? (args[0] ?? null) : null;
|
|
65
75
|
};
|
|
66
76
|
|
|
@@ -115,12 +125,12 @@ const createDiagnostic = (
|
|
|
115
125
|
filePath: string,
|
|
116
126
|
sourceCode: string,
|
|
117
127
|
node: AstNode,
|
|
118
|
-
|
|
128
|
+
ownerLabel: string,
|
|
119
129
|
variableName: string
|
|
120
130
|
): WardenDiagnostic => ({
|
|
121
131
|
filePath,
|
|
122
132
|
line: offsetToLine(sourceCode, node.start),
|
|
123
|
-
message:
|
|
133
|
+
message: `${ownerLabel}: Result.err(${variableName}.error) re-wraps a Result that already carries that error. Return ${variableName} directly to preserve the original Result boundary.`,
|
|
124
134
|
rule: RULE_NAME,
|
|
125
135
|
severity: 'warn',
|
|
126
136
|
});
|
|
@@ -133,7 +143,8 @@ const trackVariableDeclarator = (
|
|
|
133
143
|
scopedHelpers: ScopedHelperMap,
|
|
134
144
|
scopes: readonly ReadonlySet<string>[]
|
|
135
145
|
): void => {
|
|
136
|
-
const
|
|
146
|
+
const id = getNodeId(node);
|
|
147
|
+
const init = getNodeInit(node);
|
|
137
148
|
const name = identifierName(id);
|
|
138
149
|
if (!name) {
|
|
139
150
|
return;
|
|
@@ -166,11 +177,9 @@ const trackAssignmentExpression = (
|
|
|
166
177
|
scopedHelpers: ScopedHelperMap,
|
|
167
178
|
scopes: readonly ReadonlySet<string>[]
|
|
168
179
|
): void => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
right?: AstNode;
|
|
173
|
-
};
|
|
180
|
+
const left = getNodeLeft(node);
|
|
181
|
+
const operator = getNodeOperator(node);
|
|
182
|
+
const right = getNodeRight(node);
|
|
174
183
|
const name = identifierName(left);
|
|
175
184
|
if (!name) {
|
|
176
185
|
return;
|
|
@@ -202,10 +211,10 @@ const checkReturnStatement = (
|
|
|
202
211
|
scopes: readonly ReadonlySet<string>[],
|
|
203
212
|
filePath: string,
|
|
204
213
|
sourceCode: string,
|
|
205
|
-
|
|
214
|
+
ownerLabel: string,
|
|
206
215
|
diagnostics: WardenDiagnostic[]
|
|
207
216
|
): void => {
|
|
208
|
-
const
|
|
217
|
+
const argument = getNodeArgument(node);
|
|
209
218
|
if (!argument || !isResultErrCall(argument)) {
|
|
210
219
|
return;
|
|
211
220
|
}
|
|
@@ -218,13 +227,21 @@ const checkReturnStatement = (
|
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
diagnostics.push(
|
|
221
|
-
createDiagnostic(filePath, sourceCode, argument,
|
|
230
|
+
createDiagnostic(filePath, sourceCode, argument, ownerLabel, variableName)
|
|
222
231
|
);
|
|
223
232
|
};
|
|
224
233
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
const functionBody = (node: AstNode): AstNode | null => {
|
|
235
|
+
const body = getNodeBodyNode(node);
|
|
236
|
+
return body &&
|
|
237
|
+
(body.type === 'BlockStatement' || body.type === 'FunctionBody')
|
|
238
|
+
? body
|
|
239
|
+
: null;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const checkFunctionBody = (
|
|
243
|
+
owner: AstNode,
|
|
244
|
+
ownerLabel: string,
|
|
228
245
|
filePath: string,
|
|
229
246
|
sourceCode: string,
|
|
230
247
|
helperNames: ReadonlySet<string>,
|
|
@@ -232,17 +249,14 @@ const checkBlazeBody = (
|
|
|
232
249
|
resultTypeNames: ReadonlySet<string>,
|
|
233
250
|
diagnostics: WardenDiagnostic[]
|
|
234
251
|
): void => {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
!body ||
|
|
238
|
-
(body.type !== 'BlockStatement' && body.type !== 'FunctionBody')
|
|
239
|
-
) {
|
|
252
|
+
const body = functionBody(owner);
|
|
253
|
+
if (!body) {
|
|
240
254
|
return;
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
const provenance: ResultProvenance = new Map();
|
|
244
258
|
const scopedHelpers: MutableScopedHelperMap = new Map();
|
|
245
|
-
const implScope = collectScopeFrameBindings(
|
|
259
|
+
const implScope = collectScopeFrameBindings(owner);
|
|
246
260
|
const initialScopes = implScope.size > 0 ? [implScope] : [];
|
|
247
261
|
|
|
248
262
|
walkWithScopes(
|
|
@@ -284,7 +298,7 @@ const checkBlazeBody = (
|
|
|
284
298
|
scopes,
|
|
285
299
|
filePath,
|
|
286
300
|
sourceCode,
|
|
287
|
-
|
|
301
|
+
ownerLabel,
|
|
288
302
|
diagnostics
|
|
289
303
|
);
|
|
290
304
|
}
|
|
@@ -293,6 +307,28 @@ const checkBlazeBody = (
|
|
|
293
307
|
);
|
|
294
308
|
};
|
|
295
309
|
|
|
310
|
+
const isFunctionLike = (node: AstNode): boolean =>
|
|
311
|
+
node.type === 'FunctionDeclaration' ||
|
|
312
|
+
node.type === 'FunctionExpression' ||
|
|
313
|
+
node.type === 'ArrowFunctionExpression';
|
|
314
|
+
|
|
315
|
+
const functionName = (node: AstNode, context: AstParentContext): string => {
|
|
316
|
+
const declaredName = identifierName(getNodeId(node));
|
|
317
|
+
if (declaredName) {
|
|
318
|
+
return declaredName;
|
|
319
|
+
}
|
|
320
|
+
if (context.parent?.type === 'VariableDeclarator') {
|
|
321
|
+
const assignedName = identifierName(getNodeId(context.parent));
|
|
322
|
+
if (assignedName) {
|
|
323
|
+
return assignedName;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return 'anonymous function';
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const functionOwnerLabel = (node: AstNode, context: AstParentContext): string =>
|
|
330
|
+
`Function "${functionName(node, context)}"`;
|
|
331
|
+
|
|
296
332
|
export const noRedundantResultErrorWrap: WardenRule = {
|
|
297
333
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
298
334
|
if (isTestFile(filePath)) {
|
|
@@ -308,11 +344,13 @@ export const noRedundantResultErrorWrap: WardenRule = {
|
|
|
308
344
|
const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
|
|
309
345
|
const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
|
|
310
346
|
const resultTypeNames = collectResultTypeNames(ast);
|
|
347
|
+
const blazeStarts = new Set<number>();
|
|
311
348
|
for (const def of findTrailDefinitions(ast)) {
|
|
312
349
|
for (const blaze of findBlazeBodies(def.config)) {
|
|
313
|
-
|
|
350
|
+
blazeStarts.add(blaze.start);
|
|
351
|
+
checkFunctionBody(
|
|
314
352
|
blaze,
|
|
315
|
-
def.id
|
|
353
|
+
`Trail "${def.id}"`,
|
|
316
354
|
filePath,
|
|
317
355
|
sourceCode,
|
|
318
356
|
helperNames,
|
|
@@ -322,10 +360,25 @@ export const noRedundantResultErrorWrap: WardenRule = {
|
|
|
322
360
|
);
|
|
323
361
|
}
|
|
324
362
|
}
|
|
363
|
+
walkWithParents(ast, (node, context) => {
|
|
364
|
+
if (!isFunctionLike(node) || blazeStarts.has(node.start)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
checkFunctionBody(
|
|
368
|
+
node,
|
|
369
|
+
functionOwnerLabel(node, context),
|
|
370
|
+
filePath,
|
|
371
|
+
sourceCode,
|
|
372
|
+
helperNames,
|
|
373
|
+
namespaceHelpers,
|
|
374
|
+
resultTypeNames,
|
|
375
|
+
diagnostics
|
|
376
|
+
);
|
|
377
|
+
});
|
|
325
378
|
return diagnostics;
|
|
326
379
|
},
|
|
327
380
|
description:
|
|
328
|
-
'Warn when
|
|
381
|
+
'Warn when code re-wraps an existing Result error with Result.err(x.error) instead of returning the Result directly.',
|
|
329
382
|
name: RULE_NAME,
|
|
330
383
|
severity: 'warn',
|
|
331
384
|
};
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { resultAccessorNames } from '@ontrails/core';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getNodeArgument,
|
|
5
|
+
getNodeAlternate,
|
|
6
|
+
getNodeBodyNode,
|
|
7
|
+
getNodeBodyStatements,
|
|
8
|
+
getNodeConsequent,
|
|
9
|
+
getNodeDeclarations,
|
|
10
|
+
getNodeElements,
|
|
11
|
+
getNodeExpression,
|
|
12
|
+
getNodeId,
|
|
13
|
+
getNodeInit,
|
|
14
|
+
getNodeKey,
|
|
15
|
+
getNodeKind,
|
|
16
|
+
getNodeLeft,
|
|
17
|
+
getNodeName,
|
|
18
|
+
getNodeObject,
|
|
19
|
+
getNodeOperator,
|
|
20
|
+
getNodeParam,
|
|
21
|
+
getNodeParams,
|
|
22
|
+
getNodeProperties,
|
|
23
|
+
getNodeProperty,
|
|
24
|
+
getNodeRight,
|
|
25
|
+
getNodeValueNode,
|
|
26
|
+
identifierName,
|
|
27
|
+
isBlazeCall,
|
|
28
|
+
offsetToLine,
|
|
29
|
+
parse,
|
|
30
|
+
} from './ast.js';
|
|
4
31
|
import type { AstNode } from './ast.js';
|
|
5
32
|
import { isFrameworkInternalFile, isTestFile } from './scan.js';
|
|
6
33
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
@@ -104,11 +131,9 @@ const isBranchOfConditional = (outer: AstNode, parent: AstNode): boolean => {
|
|
|
104
131
|
if (parent.type !== 'ConditionalExpression') {
|
|
105
132
|
return false;
|
|
106
133
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
|
111
|
-
return cond.consequent === outer || cond.alternate === outer;
|
|
134
|
+
return (
|
|
135
|
+
getNodeConsequent(parent) === outer || getNodeAlternate(parent) === outer
|
|
136
|
+
);
|
|
112
137
|
};
|
|
113
138
|
|
|
114
139
|
/**
|
|
@@ -121,8 +146,7 @@ const isOperandOfLogical = (outer: AstNode, parent: AstNode): boolean => {
|
|
|
121
146
|
if (parent.type !== 'LogicalExpression') {
|
|
122
147
|
return false;
|
|
123
148
|
}
|
|
124
|
-
|
|
125
|
-
return logical.left === outer || logical.right === outer;
|
|
149
|
+
return getNodeLeft(parent) === outer || getNodeRight(parent) === outer;
|
|
126
150
|
};
|
|
127
151
|
|
|
128
152
|
const skipParensAndBranchConditionals = (
|
|
@@ -165,11 +189,11 @@ const memberPropertyName = (node: AstNode): string | null => {
|
|
|
165
189
|
) {
|
|
166
190
|
return null;
|
|
167
191
|
}
|
|
168
|
-
const prop = (node
|
|
192
|
+
const prop = getNodeProperty(node);
|
|
169
193
|
if (prop?.type !== 'Identifier') {
|
|
170
194
|
return null;
|
|
171
195
|
}
|
|
172
|
-
return (prop
|
|
196
|
+
return getNodeName(prop) ?? null;
|
|
173
197
|
};
|
|
174
198
|
|
|
175
199
|
/**
|
|
@@ -208,7 +232,7 @@ const extractAssignedBinding = (
|
|
|
208
232
|
if (!parent || parent.type !== 'VariableDeclarator') {
|
|
209
233
|
return null;
|
|
210
234
|
}
|
|
211
|
-
const
|
|
235
|
+
const id = getNodeId(parent);
|
|
212
236
|
return identifierName(id);
|
|
213
237
|
};
|
|
214
238
|
|
|
@@ -231,7 +255,7 @@ const isResultAccessorMember = (node: AstNode): boolean => {
|
|
|
231
255
|
};
|
|
232
256
|
|
|
233
257
|
const getIdentifierObjectName = (node: AstNode): string | null => {
|
|
234
|
-
const
|
|
258
|
+
const object = getNodeObject(node);
|
|
235
259
|
return object?.type === 'Identifier' ? identifierName(object) : null;
|
|
236
260
|
};
|
|
237
261
|
|
|
@@ -250,7 +274,7 @@ const collectAssignmentPatternBindings = (
|
|
|
250
274
|
pattern: AstNode,
|
|
251
275
|
out: Set<string>
|
|
252
276
|
): void => {
|
|
253
|
-
const
|
|
277
|
+
const left = getNodeLeft(pattern);
|
|
254
278
|
// eslint-disable-next-line no-use-before-define
|
|
255
279
|
collectPatternBindings(left, out);
|
|
256
280
|
};
|
|
@@ -259,7 +283,7 @@ const collectRestElementBindings = (
|
|
|
259
283
|
pattern: AstNode,
|
|
260
284
|
out: Set<string>
|
|
261
285
|
): void => {
|
|
262
|
-
const
|
|
286
|
+
const argument = getNodeArgument(pattern);
|
|
263
287
|
// eslint-disable-next-line no-use-before-define
|
|
264
288
|
collectPatternBindings(argument, out);
|
|
265
289
|
};
|
|
@@ -303,9 +327,7 @@ const collectArrayPatternBindings = (
|
|
|
303
327
|
pattern: AstNode,
|
|
304
328
|
out: Set<string>
|
|
305
329
|
): void => {
|
|
306
|
-
const
|
|
307
|
-
elements?: readonly (AstNode | null)[];
|
|
308
|
-
};
|
|
330
|
+
const elements = getNodeElements(pattern);
|
|
309
331
|
if (!elements) {
|
|
310
332
|
return;
|
|
311
333
|
}
|
|
@@ -321,9 +343,7 @@ const collectObjectPatternBindings = (
|
|
|
321
343
|
pattern: AstNode,
|
|
322
344
|
out: Set<string>
|
|
323
345
|
): void => {
|
|
324
|
-
const
|
|
325
|
-
properties?: readonly AstNode[];
|
|
326
|
-
};
|
|
346
|
+
const properties = getNodeProperties(pattern);
|
|
327
347
|
if (!properties) {
|
|
328
348
|
return;
|
|
329
349
|
}
|
|
@@ -333,7 +353,7 @@ const collectObjectPatternBindings = (
|
|
|
333
353
|
collectPatternBindings(prop, out);
|
|
334
354
|
} else {
|
|
335
355
|
// Property node: value holds the binding pattern.
|
|
336
|
-
const
|
|
356
|
+
const value = getNodeValueNode(prop);
|
|
337
357
|
// eslint-disable-next-line no-use-before-define
|
|
338
358
|
collectPatternBindings(value, out);
|
|
339
359
|
}
|
|
@@ -380,16 +400,12 @@ const collectVariableDeclarationBindings = (
|
|
|
380
400
|
if (!declNode || declNode.type !== 'VariableDeclaration') {
|
|
381
401
|
return;
|
|
382
402
|
}
|
|
383
|
-
const declarators = (
|
|
384
|
-
declNode as unknown as {
|
|
385
|
-
declarations?: readonly AstNode[];
|
|
386
|
-
}
|
|
387
|
-
).declarations;
|
|
403
|
+
const declarators = getNodeDeclarations(declNode);
|
|
388
404
|
if (!declarators) {
|
|
389
405
|
return;
|
|
390
406
|
}
|
|
391
407
|
for (const d of declarators) {
|
|
392
|
-
const
|
|
408
|
+
const id = getNodeId(d);
|
|
393
409
|
collectPatternBindings(id, out);
|
|
394
410
|
}
|
|
395
411
|
};
|
|
@@ -400,7 +416,7 @@ const getVariableDeclarationKind = (
|
|
|
400
416
|
if (!declNode || declNode.type !== 'VariableDeclaration') {
|
|
401
417
|
return null;
|
|
402
418
|
}
|
|
403
|
-
return (declNode
|
|
419
|
+
return getNodeKind(declNode) ?? null;
|
|
404
420
|
};
|
|
405
421
|
|
|
406
422
|
/** True if declaration is `var` (function/program-scoped, hoistable). */
|
|
@@ -426,7 +442,7 @@ interface FunctionScopeBindings {
|
|
|
426
442
|
|
|
427
443
|
const collectParamBindings = (scope: AstNode): Set<string> => {
|
|
428
444
|
const paramBindings = new Set<string>();
|
|
429
|
-
const
|
|
445
|
+
const params = getNodeParams(scope);
|
|
430
446
|
if (params) {
|
|
431
447
|
for (const param of params) {
|
|
432
448
|
collectPatternBindings(param, paramBindings);
|
|
@@ -436,7 +452,7 @@ const collectParamBindings = (scope: AstNode): Set<string> => {
|
|
|
436
452
|
};
|
|
437
453
|
|
|
438
454
|
const addHoistedVarsFromBody = (scope: AstNode, out: Set<string>): void => {
|
|
439
|
-
const
|
|
455
|
+
const body = getNodeBodyNode(scope);
|
|
440
456
|
if (!(body && isAstLike(body))) {
|
|
441
457
|
return;
|
|
442
458
|
}
|
|
@@ -462,7 +478,7 @@ const collectFunctionScopeBindings = (scope: AstNode): Set<string> =>
|
|
|
462
478
|
|
|
463
479
|
const collectCatchScopeBindings = (scope: AstNode): Set<string> => {
|
|
464
480
|
const bindings = new Set<string>();
|
|
465
|
-
const
|
|
481
|
+
const param = getNodeParam(scope);
|
|
466
482
|
collectPatternBindings(param, bindings);
|
|
467
483
|
return bindings;
|
|
468
484
|
};
|
|
@@ -470,10 +486,10 @@ const collectCatchScopeBindings = (scope: AstNode): Set<string> => {
|
|
|
470
486
|
const collectForScopeBindings = (scope: AstNode): Set<string> => {
|
|
471
487
|
const bindings = new Set<string>();
|
|
472
488
|
if (scope.type === 'ForStatement') {
|
|
473
|
-
const
|
|
489
|
+
const init = getNodeInit(scope);
|
|
474
490
|
collectBlockScopedDeclaratorBindings(init, bindings);
|
|
475
491
|
} else {
|
|
476
|
-
const
|
|
492
|
+
const left = getNodeLeft(scope);
|
|
477
493
|
collectBlockScopedDeclaratorBindings(left, bindings);
|
|
478
494
|
}
|
|
479
495
|
return bindings;
|
|
@@ -483,7 +499,7 @@ const addFunctionDeclarationName = (stmt: AstNode, out: Set<string>): void => {
|
|
|
483
499
|
if (stmt.type !== 'FunctionDeclaration') {
|
|
484
500
|
return;
|
|
485
501
|
}
|
|
486
|
-
const
|
|
502
|
+
const id = getNodeId(stmt);
|
|
487
503
|
const fnName = identifierName(id);
|
|
488
504
|
if (fnName) {
|
|
489
505
|
out.add(fnName);
|
|
@@ -494,7 +510,7 @@ const addClassDeclarationName = (stmt: AstNode, out: Set<string>): void => {
|
|
|
494
510
|
if (stmt.type !== 'ClassDeclaration') {
|
|
495
511
|
return;
|
|
496
512
|
}
|
|
497
|
-
const
|
|
513
|
+
const id = getNodeId(stmt);
|
|
498
514
|
const className = identifierName(id);
|
|
499
515
|
if (className) {
|
|
500
516
|
out.add(className);
|
|
@@ -517,7 +533,7 @@ const collectBlockScopedStatementListBindings = (
|
|
|
517
533
|
|
|
518
534
|
const collectBlockStatementBindings = (scope: AstNode): Set<string> => {
|
|
519
535
|
const bindings = new Set<string>();
|
|
520
|
-
const
|
|
536
|
+
const body = getNodeBodyStatements(scope);
|
|
521
537
|
collectBlockScopedStatementListBindings(body, bindings);
|
|
522
538
|
// Static initializer blocks own their own VariableEnvironment (per ES spec),
|
|
523
539
|
// so `var` declarations inside them do not escape into the enclosing class
|
|
@@ -707,10 +723,8 @@ const extractPlainIdentifierAssignmentName = (
|
|
|
707
723
|
if (!parent || parent.type !== 'AssignmentExpression') {
|
|
708
724
|
return null;
|
|
709
725
|
}
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
left?: AstNode;
|
|
713
|
-
};
|
|
726
|
+
const operator = getNodeOperator(parent);
|
|
727
|
+
const left = getNodeLeft(parent);
|
|
714
728
|
// Only plain `=` assignments to a bare identifier. Member-expression LHS
|
|
715
729
|
// (`obj.result = blaze(...)`) is a property write, not a bare binding we
|
|
716
730
|
// can track by name.
|
|
@@ -759,10 +773,8 @@ const isAssignmentToParamName = (node: AstNode): boolean => {
|
|
|
759
773
|
if (node.type !== 'AssignmentExpression') {
|
|
760
774
|
return false;
|
|
761
775
|
}
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
left?: AstNode;
|
|
765
|
-
};
|
|
776
|
+
const operator = getNodeOperator(node);
|
|
777
|
+
const left = getNodeLeft(node);
|
|
766
778
|
return operator === '=' && left?.type === 'Identifier';
|
|
767
779
|
};
|
|
768
780
|
|
|
@@ -834,24 +846,19 @@ type CarrierChildExtractor = (
|
|
|
834
846
|
) => readonly (AstNode | undefined)[];
|
|
835
847
|
|
|
836
848
|
const CARRIER_CHILDREN: Record<string, CarrierChildExtractor> = {
|
|
837
|
-
ConditionalExpression: (expr) =>
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
};
|
|
842
|
-
return [consequent, alternate];
|
|
843
|
-
},
|
|
849
|
+
ConditionalExpression: (expr) => [
|
|
850
|
+
getNodeConsequent(expr),
|
|
851
|
+
getNodeAlternate(expr),
|
|
852
|
+
],
|
|
844
853
|
LogicalExpression: (expr) => {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
right?: AstNode;
|
|
848
|
-
};
|
|
854
|
+
const left = getNodeLeft(expr);
|
|
855
|
+
const right = getNodeRight(expr);
|
|
849
856
|
return [left, right];
|
|
850
857
|
},
|
|
851
858
|
};
|
|
852
859
|
|
|
853
860
|
const unwrapTransparentWrapper = (expr: AstNode): AstNode | undefined =>
|
|
854
|
-
(expr
|
|
861
|
+
getNodeExpression(expr);
|
|
855
862
|
|
|
856
863
|
// biome-ignore lint/style/useConst: hoisted for recursive call
|
|
857
864
|
// eslint-disable-next-line func-style
|
|
@@ -893,11 +900,9 @@ const extractIdentifierAssignment = (
|
|
|
893
900
|
if (node.type !== 'AssignmentExpression') {
|
|
894
901
|
return null;
|
|
895
902
|
}
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
right?: AstNode;
|
|
900
|
-
};
|
|
903
|
+
const operator = getNodeOperator(node);
|
|
904
|
+
const left = getNodeLeft(node);
|
|
905
|
+
const right = getNodeRight(node);
|
|
901
906
|
if (!(operator && left) || left.type !== 'Identifier') {
|
|
902
907
|
return null;
|
|
903
908
|
}
|
|
@@ -1012,15 +1017,12 @@ const propertyDestructuresResultAccessor = (prop: AstNode): boolean => {
|
|
|
1012
1017
|
if (prop.type === 'RestElement') {
|
|
1013
1018
|
return false;
|
|
1014
1019
|
}
|
|
1015
|
-
const
|
|
1016
|
-
const keyName = identifierName(key);
|
|
1020
|
+
const keyName = identifierName(getNodeKey(prop));
|
|
1017
1021
|
return keyName !== null && RESULT_ACCESSOR_PROPERTIES.has(keyName);
|
|
1018
1022
|
};
|
|
1019
1023
|
|
|
1020
1024
|
const objectPatternHasResultAccessorKey = (pattern: AstNode): boolean => {
|
|
1021
|
-
const
|
|
1022
|
-
properties?: readonly AstNode[];
|
|
1023
|
-
};
|
|
1025
|
+
const properties = getNodeProperties(pattern);
|
|
1024
1026
|
return properties?.some(propertyDestructuresResultAccessor) ?? false;
|
|
1025
1027
|
};
|
|
1026
1028
|
|
|
@@ -1037,7 +1039,7 @@ const getDestructuredResultAccessorDeclarator = (
|
|
|
1037
1039
|
if (!parent || parent.type !== 'VariableDeclarator') {
|
|
1038
1040
|
return null;
|
|
1039
1041
|
}
|
|
1040
|
-
const
|
|
1042
|
+
const id = getNodeId(parent);
|
|
1041
1043
|
if (!id || id.type !== 'ObjectPattern') {
|
|
1042
1044
|
return null;
|
|
1043
1045
|
}
|
|
@@ -1138,7 +1140,7 @@ function walkWithScopes(node: AstNode, state: AnalyzeState): void {
|
|
|
1138
1140
|
|
|
1139
1141
|
const collectProgramBindings = (ast: AstNode): Set<string> => {
|
|
1140
1142
|
const bindings = new Set<string>();
|
|
1141
|
-
const programBody = (ast
|
|
1143
|
+
const programBody = getNodeBodyStatements(ast);
|
|
1142
1144
|
// Top-level `let`/`const`/function declarations.
|
|
1143
1145
|
collectBlockScopedStatementListBindings(programBody, bindings);
|
|
1144
1146
|
// Top-level `var`s are program-scoped; also hoist any `var`s nested
|