@ontrails/warden 1.0.0-beta.5 → 1.0.0-beta.7
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +22 -0
- package/dist/rules/ast.d.ts +12 -5
- package/dist/rules/ast.d.ts.map +1 -1
- package/dist/rules/ast.js +98 -41
- package/dist/rules/ast.js.map +1 -1
- package/dist/rules/follow-declarations.d.ts.map +1 -1
- package/dist/rules/follow-declarations.js +88 -19
- package/dist/rules/follow-declarations.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/follow-declarations.test.ts +152 -0
- package/src/rules/ast.ts +123 -50
- package/src/rules/follow-declarations.ts +117 -20
package/.turbo/turbo-lint.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @ontrails/warden
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @ontrails/schema@1.0.0-beta.7
|
|
9
|
+
- @ontrails/core@1.0.0-beta.7
|
|
10
|
+
|
|
11
|
+
## 1.0.0-beta.6
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Fix Codex review findings on type-utils and follow-declarations.
|
|
16
|
+
|
|
17
|
+
**core**: `inputOf()`/`outputOf()` now preserve the exact Zod schema subtype instead of widening to `z.ZodType`.
|
|
18
|
+
|
|
19
|
+
**warden**: `follow-declarations` rule now recognizes single-object trail overload, detects any context parameter name (not just `ctx`), matches destructured `follow()` calls, resolves const identifiers in `follow` arrays, and restricts run body extraction to top-level config properties.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @ontrails/core@1.0.0-beta.6
|
|
23
|
+
- @ontrails/schema@1.0.0-beta.6
|
|
24
|
+
|
|
3
25
|
## 1.0.0-beta.5
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/dist/rules/ast.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export declare const parse: (filePath: string, sourceCode: string) => AstNode |
|
|
|
21
21
|
export declare const walk: (node: unknown, visit: (node: AstNode) => void) => void;
|
|
22
22
|
/** Find the byte offset's line number (1-based) in source code. */
|
|
23
23
|
export declare const offsetToLine: (sourceCode: string, offset: number) => number;
|
|
24
|
-
/** Find
|
|
25
|
-
export declare const
|
|
24
|
+
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
25
|
+
export declare const findConfigProperty: (config: AstNode, propertyName: string) => AstNode | null;
|
|
26
26
|
export interface TrailDefinition {
|
|
27
27
|
/** Trail ID string, e.g. "entity.show" */
|
|
28
28
|
readonly id: string;
|
|
@@ -33,9 +33,16 @@ export interface TrailDefinition {
|
|
|
33
33
|
/** Start offset of the call expression */
|
|
34
34
|
readonly start: number;
|
|
35
35
|
}
|
|
36
|
+
export declare const findTrailDefinitions: (ast: AstNode) => TrailDefinition[];
|
|
37
|
+
/**
|
|
38
|
+
* Find `run:` property values.
|
|
39
|
+
*
|
|
40
|
+
* When given an ObjectExpression (trail config), returns only its direct `run:`
|
|
41
|
+
* properties. When given a full AST, finds trail definitions first and extracts
|
|
42
|
+
* `run:` from each config — in both cases ignoring nested `run:` properties
|
|
43
|
+
* (e.g. `metadata: { run: ... }`).
|
|
44
|
+
*/
|
|
45
|
+
export declare const findRunBodies: (node: AstNode) => AstNode[];
|
|
36
46
|
/** Check if a node is a call to `.run()` on some object. */
|
|
37
47
|
export declare const isRunCall: (node: AstNode) => boolean;
|
|
38
|
-
export declare const findTrailDefinitions: (ast: AstNode) => TrailDefinition[];
|
|
39
|
-
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
40
|
-
export declare const findConfigProperty: (config: AstNode, propertyName: string) => AstNode | null;
|
|
41
48
|
//# sourceMappingURL=ast.d.ts.map
|
package/dist/rules/ast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/rules/ast.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,CAAC;IAC7C,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAMD,0EAA0E;AAC1E,eAAO,MAAM,KAAK,GAAI,UAAU,MAAM,EAAE,YAAY,MAAM,KAAG,OAAO,GAAG,IAOtE,CAAC;AAMF,4DAA4D;AAC5D,eAAO,MAAM,IAAI,GAAI,MAAM,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAG,IAiBpE,CAAC;AAMF,mEAAmE;AACnE,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,EAAE,QAAQ,MAAM,KAAG,MAQjE,CAAC;
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/rules/ast.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,CAAC;IAC7C,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAMD,0EAA0E;AAC1E,eAAO,MAAM,KAAK,GAAI,UAAU,MAAM,EAAE,YAAY,MAAM,KAAG,OAAO,GAAG,IAOtE,CAAC;AAMF,4DAA4D;AAC5D,eAAO,MAAM,IAAI,GAAI,MAAM,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAG,IAiBpE,CAAC;AAMF,mEAAmE;AACnE,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,EAAE,QAAQ,MAAM,KAAG,MAQjE,CAAC;AAMF,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,OAAO,EACf,cAAc,MAAM,KACnB,OAAO,GAAG,IAcZ,CAAC;AAMF,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AA2FD,eAAO,MAAM,oBAAoB,GAAI,KAAK,OAAO,KAAG,eAAe,EAWlE,CAAC;AAyBF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,OAAO,KAAG,OAAO,EAWpD,CAAC;AAMF,4DAA4D;AAC5D,eAAO,MAAM,SAAS,GAAI,MAAM,OAAO,KAAG,OAmBzC,CAAC"}
|
package/dist/rules/ast.js
CHANGED
|
@@ -54,18 +54,28 @@ export const offsetToLine = (sourceCode, offset) => {
|
|
|
54
54
|
}
|
|
55
55
|
return line;
|
|
56
56
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Config property extraction helpers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
61
|
+
export const findConfigProperty = (config, propertyName) => {
|
|
62
|
+
if (config.type !== 'ObjectExpression') {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const properties = config['properties'];
|
|
66
|
+
if (!properties) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
for (const prop of properties) {
|
|
70
|
+
if (prop.type === 'Property' && prop.key?.name === propertyName) {
|
|
71
|
+
return prop;
|
|
63
72
|
}
|
|
64
|
-
}
|
|
65
|
-
return
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
66
75
|
};
|
|
67
76
|
/**
|
|
68
|
-
* Find all `trail("id", { ... })
|
|
77
|
+
* Find all `trail("id", { ... })`, `trail({ id: "x", ... })`, and
|
|
78
|
+
* `event("id", { ... })` call sites.
|
|
69
79
|
*
|
|
70
80
|
* Returns the trail ID, kind, and config object node for each definition.
|
|
71
81
|
*/
|
|
@@ -81,16 +91,39 @@ const getTrailCalleeName = (node) => {
|
|
|
81
91
|
const { name } = callee;
|
|
82
92
|
return name && TRAIL_CALLEE_NAMES.has(name) ? name : null;
|
|
83
93
|
};
|
|
94
|
+
/** Extract args from a trail() call, handling both two-arg and single-object forms. */
|
|
84
95
|
const extractTrailArgs = (node) => {
|
|
85
96
|
const args = node['arguments'];
|
|
86
|
-
if (!args || args.length
|
|
97
|
+
if (!args || args.length === 0) {
|
|
87
98
|
return null;
|
|
88
99
|
}
|
|
89
|
-
const [
|
|
90
|
-
if (!
|
|
100
|
+
const [firstArg, secondArg] = args;
|
|
101
|
+
if (!firstArg) {
|
|
91
102
|
return null;
|
|
92
103
|
}
|
|
93
|
-
|
|
104
|
+
// Two-arg form: trail('id', { ... })
|
|
105
|
+
if (secondArg && firstArg.type !== 'ObjectExpression') {
|
|
106
|
+
return { configArg: secondArg, idArg: firstArg };
|
|
107
|
+
}
|
|
108
|
+
// Single-object form: trail({ id: 'x', ... })
|
|
109
|
+
return firstArg.type === 'ObjectExpression'
|
|
110
|
+
? { configArg: firstArg, idArg: null }
|
|
111
|
+
: null;
|
|
112
|
+
};
|
|
113
|
+
/** Extract the string value from an `id` property inside a config ObjectExpression. */
|
|
114
|
+
const extractIdFromConfig = (config) => {
|
|
115
|
+
const idProp = findConfigProperty(config, 'id');
|
|
116
|
+
if (!idProp || !idProp.value) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const val = idProp.value.value;
|
|
120
|
+
return typeof val === 'string' ? val : null;
|
|
121
|
+
};
|
|
122
|
+
const extractTrailId = (trailArgs) => {
|
|
123
|
+
if (trailArgs.idArg) {
|
|
124
|
+
return trailArgs.idArg.value ?? null;
|
|
125
|
+
}
|
|
126
|
+
return extractIdFromConfig(trailArgs.configArg);
|
|
94
127
|
};
|
|
95
128
|
const extractTrailDefinition = (node) => {
|
|
96
129
|
const calleeName = getTrailCalleeName(node);
|
|
@@ -101,7 +134,7 @@ const extractTrailDefinition = (node) => {
|
|
|
101
134
|
if (!trailArgs) {
|
|
102
135
|
return null;
|
|
103
136
|
}
|
|
104
|
-
const trailId = trailArgs
|
|
137
|
+
const trailId = extractTrailId(trailArgs);
|
|
105
138
|
if (!trailId) {
|
|
106
139
|
return null;
|
|
107
140
|
}
|
|
@@ -112,23 +145,6 @@ const extractTrailDefinition = (node) => {
|
|
|
112
145
|
start: node.start,
|
|
113
146
|
};
|
|
114
147
|
};
|
|
115
|
-
/** Check if a node is a call to `.run()` on some object. */
|
|
116
|
-
export const isRunCall = (node) => {
|
|
117
|
-
if (node.type !== 'CallExpression') {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
const callee = node['callee'];
|
|
121
|
-
if (!callee) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
if (callee.type !== 'StaticMemberExpression' &&
|
|
125
|
-
callee.type !== 'MemberExpression') {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
const prop = callee.property;
|
|
129
|
-
return (prop?.type === 'Identifier' &&
|
|
130
|
-
prop.name === 'run');
|
|
131
|
-
};
|
|
132
148
|
export const findTrailDefinitions = (ast) => {
|
|
133
149
|
const definitions = [];
|
|
134
150
|
walk(ast, (node) => {
|
|
@@ -140,22 +156,63 @@ export const findTrailDefinitions = (ast) => {
|
|
|
140
156
|
return definitions;
|
|
141
157
|
};
|
|
142
158
|
// ---------------------------------------------------------------------------
|
|
143
|
-
//
|
|
159
|
+
// Run body extraction
|
|
144
160
|
// ---------------------------------------------------------------------------
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Extract top-level `run:` property values from an ObjectExpression's direct properties.
|
|
163
|
+
*
|
|
164
|
+
* Does not recurse into nested objects, so `metadata: { run: ... }` is ignored.
|
|
165
|
+
*/
|
|
166
|
+
const extractRunFromConfig = (config) => {
|
|
167
|
+
const bodies = [];
|
|
150
168
|
const properties = config['properties'];
|
|
151
169
|
if (!properties) {
|
|
152
|
-
return
|
|
170
|
+
return bodies;
|
|
153
171
|
}
|
|
154
172
|
for (const prop of properties) {
|
|
155
|
-
if (prop.type === 'Property' && prop.key?.name ===
|
|
156
|
-
|
|
173
|
+
if (prop.type === 'Property' && prop.key?.name === 'run' && prop.value) {
|
|
174
|
+
bodies.push(prop.value);
|
|
157
175
|
}
|
|
158
176
|
}
|
|
159
|
-
return
|
|
177
|
+
return bodies;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Find `run:` property values.
|
|
181
|
+
*
|
|
182
|
+
* When given an ObjectExpression (trail config), returns only its direct `run:`
|
|
183
|
+
* properties. When given a full AST, finds trail definitions first and extracts
|
|
184
|
+
* `run:` from each config — in both cases ignoring nested `run:` properties
|
|
185
|
+
* (e.g. `metadata: { run: ... }`).
|
|
186
|
+
*/
|
|
187
|
+
export const findRunBodies = (node) => {
|
|
188
|
+
if (node.type === 'ObjectExpression') {
|
|
189
|
+
return extractRunFromConfig(node);
|
|
190
|
+
}
|
|
191
|
+
// Full AST — find trail definitions and extract run from their configs
|
|
192
|
+
const bodies = [];
|
|
193
|
+
for (const def of findTrailDefinitions(node)) {
|
|
194
|
+
bodies.push(...extractRunFromConfig(def.config));
|
|
195
|
+
}
|
|
196
|
+
return bodies;
|
|
197
|
+
};
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Misc helpers
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
/** Check if a node is a call to `.run()` on some object. */
|
|
202
|
+
export const isRunCall = (node) => {
|
|
203
|
+
if (node.type !== 'CallExpression') {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const callee = node['callee'];
|
|
207
|
+
if (!callee) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (callee.type !== 'StaticMemberExpression' &&
|
|
211
|
+
callee.type !== 'MemberExpression') {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const prop = callee.property;
|
|
215
|
+
return (prop?.type === 'Identifier' &&
|
|
216
|
+
prop.name === 'run');
|
|
160
217
|
};
|
|
161
218
|
//# sourceMappingURL=ast.js.map
|
package/dist/rules/ast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../../src/rules/ast.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAgBvC,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,QAAgB,EAAE,UAAkB,EAAkB,EAAE;IAC5E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzE,OAAO,MAAM,CAAC,OAA6B,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAa,EAAE,KAA8B,EAAQ,EAAE;IAC1E,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO;IACT,CAAC;IACD,MAAM,CAAC,GAAG,IAAe,CAAC;IAC1B,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACX,KAAK,CAAC,CAAC,CAAC,CAAC;IACX,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;gBACvB,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAK,GAAe,CAAC,IAAI,EAAE,CAAC;YACnE,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,UAAkB,EAAE,MAAc,EAAU,EAAE;IACzE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5D,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../../src/rules/ast.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"follow-declarations.d.ts","sourceRoot":"","sources":["../../src/rules/follow-declarations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"follow-declarations.d.ts","sourceRoot":"","sources":["../../src/rules/follow-declarations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,YAAY,CAAC;AA+U/D;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,UAuBhC,CAAC"}
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
import { findConfigProperty, findRunBodies, findTrailDefinitions, offsetToLine, parse, walk, } from './ast.js';
|
|
9
9
|
import { isTestFile } from './scan.js';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
+
// Shared identifier helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Get the name of an Identifier node, or null. */
|
|
14
|
+
const identifierName = (node) => {
|
|
15
|
+
if (node?.type !== 'Identifier') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return node.name ?? null;
|
|
19
|
+
};
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
11
21
|
// String literal helpers
|
|
12
22
|
// ---------------------------------------------------------------------------
|
|
13
23
|
/** Check if a node is a string literal (covers `StringLiteral` and `Literal` with string value). */
|
|
@@ -26,6 +36,31 @@ const getStringValue = (node) => {
|
|
|
26
36
|
return typeof val === 'string' ? val : null;
|
|
27
37
|
};
|
|
28
38
|
// ---------------------------------------------------------------------------
|
|
39
|
+
// Const identifier resolution
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort resolution of `const NAME = 'value'` declarations via regex.
|
|
43
|
+
*
|
|
44
|
+
* Returns the string value if a simple `const <name> = '...'` or `"..."` is
|
|
45
|
+
* found in the source. Returns null for anything more complex.
|
|
46
|
+
*/
|
|
47
|
+
const resolveConstString = (name, sourceCode) => {
|
|
48
|
+
const pattern = new RegExp(`const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`);
|
|
49
|
+
const match = pattern.exec(sourceCode);
|
|
50
|
+
if (!match) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return match[1] ?? match[2] ?? null;
|
|
54
|
+
};
|
|
55
|
+
/** Try to resolve an Identifier element to a string via const declaration. */
|
|
56
|
+
const resolveIdentifierElement = (el, sourceCode) => {
|
|
57
|
+
const name = identifierName(el);
|
|
58
|
+
if (!name) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return resolveConstString(name, sourceCode);
|
|
62
|
+
};
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
29
64
|
// Declared follow extraction
|
|
30
65
|
// ---------------------------------------------------------------------------
|
|
31
66
|
/** Extract the ArrayExpression elements from a config's `follow` property. */
|
|
@@ -41,8 +76,8 @@ const getFollowElements = (config) => {
|
|
|
41
76
|
const elements = arrayNode['elements'];
|
|
42
77
|
return elements ?? null;
|
|
43
78
|
};
|
|
44
|
-
/** Collect string IDs from array elements. */
|
|
45
|
-
const collectStringIds = (elements) => {
|
|
79
|
+
/** Collect string IDs from array elements, resolving identifiers when possible. */
|
|
80
|
+
const collectStringIds = (elements, sourceCode) => {
|
|
46
81
|
const ids = new Set();
|
|
47
82
|
for (const el of elements) {
|
|
48
83
|
if (isStringLiteral(el)) {
|
|
@@ -51,25 +86,24 @@ const collectStringIds = (elements) => {
|
|
|
51
86
|
ids.add(val);
|
|
52
87
|
}
|
|
53
88
|
}
|
|
89
|
+
else if (el.type === 'Identifier') {
|
|
90
|
+
const resolved = resolveIdentifierElement(el, sourceCode);
|
|
91
|
+
if (resolved) {
|
|
92
|
+
ids.add(resolved);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
54
95
|
}
|
|
55
96
|
return ids;
|
|
56
97
|
};
|
|
57
98
|
/** Extract string literal elements from a `follow: [...]` array property. */
|
|
58
|
-
const extractDeclaredFollows = (config) => {
|
|
99
|
+
const extractDeclaredFollows = (config, sourceCode) => {
|
|
59
100
|
const elements = getFollowElements(config);
|
|
60
|
-
return elements ? collectStringIds(elements) : new Set();
|
|
101
|
+
return elements ? collectStringIds(elements, sourceCode) : new Set();
|
|
61
102
|
};
|
|
62
103
|
// ---------------------------------------------------------------------------
|
|
63
104
|
// Called follow extraction — member expression helpers
|
|
64
105
|
// ---------------------------------------------------------------------------
|
|
65
106
|
const MEMBER_TYPES = new Set(['StaticMemberExpression', 'MemberExpression']);
|
|
66
|
-
/** Get the name of an Identifier node, or null. */
|
|
67
|
-
const identifierName = (node) => {
|
|
68
|
-
if (node?.type !== 'Identifier') {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
return node.name ?? null;
|
|
72
|
-
};
|
|
73
107
|
/** Extract object and property Identifier names from a MemberExpression. */
|
|
74
108
|
const extractMemberPair = (callee) => {
|
|
75
109
|
if (!MEMBER_TYPES.has(callee.type)) {
|
|
@@ -92,8 +126,30 @@ const extractFirstStringArg = (node) => {
|
|
|
92
126
|
}
|
|
93
127
|
return getStringValue(firstArg);
|
|
94
128
|
};
|
|
95
|
-
/**
|
|
96
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Extract the second parameter name from a run function node.
|
|
131
|
+
*
|
|
132
|
+
* Handles `(input, ctx) => ...` and `async (input, context) => ...` and
|
|
133
|
+
* `function(input, ctx) { ... }` forms.
|
|
134
|
+
*/
|
|
135
|
+
const extractContextParamName = (runBody) => {
|
|
136
|
+
const params = runBody['params'];
|
|
137
|
+
if (!params || params.length < 2) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return identifierName(params[1]);
|
|
141
|
+
};
|
|
142
|
+
/** Check if a callee is a member-style follow call: <ctxName>.follow(...). */
|
|
143
|
+
const isMemberFollowCall = (callee, ctxNames) => {
|
|
144
|
+
const pair = extractMemberPair(callee);
|
|
145
|
+
return !!pair && ctxNames.has(pair.objName) && pair.propName === 'follow';
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Check if a node is a `<ctxName>.follow(...)` call and return the string trail ID.
|
|
149
|
+
*
|
|
150
|
+
* Also matches bare `follow(...)` calls (destructured pattern).
|
|
151
|
+
*/
|
|
152
|
+
const extractFollowCallId = (node, ctxNames) => {
|
|
97
153
|
if (node.type !== 'CallExpression') {
|
|
98
154
|
return null;
|
|
99
155
|
}
|
|
@@ -101,18 +157,31 @@ const extractFollowCallId = (node) => {
|
|
|
101
157
|
if (!callee) {
|
|
102
158
|
return null;
|
|
103
159
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
160
|
+
if (isMemberFollowCall(callee, ctxNames)) {
|
|
161
|
+
return extractFirstStringArg(node);
|
|
162
|
+
}
|
|
163
|
+
// Match bare follow(...) — destructured pattern
|
|
164
|
+
if (identifierName(callee) === 'follow') {
|
|
165
|
+
return extractFirstStringArg(node);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
/** Build the set of context parameter names to match against. */
|
|
170
|
+
const buildCtxNames = (body) => {
|
|
171
|
+
const ctxNames = new Set(['ctx', 'context']);
|
|
172
|
+
const paramName = extractContextParamName(body);
|
|
173
|
+
if (paramName) {
|
|
174
|
+
ctxNames.add(paramName);
|
|
107
175
|
}
|
|
108
|
-
return
|
|
176
|
+
return ctxNames;
|
|
109
177
|
};
|
|
110
178
|
/** Walk run bodies and collect all statically resolvable ctx.follow() trail IDs. */
|
|
111
179
|
const extractCalledFollows = (config) => {
|
|
112
180
|
const ids = new Set();
|
|
113
181
|
for (const body of findRunBodies(config)) {
|
|
182
|
+
const ctxNames = buildCtxNames(body);
|
|
114
183
|
walk(body, (node) => {
|
|
115
|
-
const id = extractFollowCallId(node);
|
|
184
|
+
const id = extractFollowCallId(node, ctxNames);
|
|
116
185
|
if (id) {
|
|
117
186
|
ids.add(id);
|
|
118
187
|
}
|
|
@@ -157,7 +226,7 @@ const reportUnused = (declared, called, ctx, diagnostics) => {
|
|
|
157
226
|
}
|
|
158
227
|
};
|
|
159
228
|
const checkTrailDefinition = (def, filePath, sourceCode, diagnostics) => {
|
|
160
|
-
const declared = extractDeclaredFollows(def.config);
|
|
229
|
+
const declared = extractDeclaredFollows(def.config, sourceCode);
|
|
161
230
|
const called = extractCalledFollows(def.config);
|
|
162
231
|
if (declared.size === 0 && called.size === 0) {
|
|
163
232
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"follow-declarations.js","sourceRoot":"","sources":["../../src/rules/follow-declarations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,KAAK,EACL,IAAI,GACL,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,oGAAoG;AACpG,MAAM,eAAe,GAAG,CAAC,IAAa,EAAW,EAAE;IACjD,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,OAAQ,IAAuC,CAAC,KAAK,KAAK,QAAQ,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,2DAA2D;AAC3D,MAAM,cAAc,GAAG,CAAC,IAAa,EAAiB,EAAE;IACtD,MAAM,GAAG,GAAI,IAAuC,CAAC,KAAK,CAAC;IAC3D,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC,CAAC;AAEF,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,CAAC,MAAe,EAA6B,EAAE;IACvE,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC;IACnC,IAAI,CAAC,SAAS,IAAK,SAAqB,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAI,SAAqB,CAAC,UAAU,CAErC,CAAC;IACd,OAAO,QAAQ,IAAI,IAAI,CAAC;AAC1B,CAAC,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"follow-declarations.js","sourceRoot":"","sources":["../../src/rules/follow-declarations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,KAAK,EACL,IAAI,GACL,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,8EAA8E;AAC9E,4BAA4B;AAC5B,8EAA8E;AAE9E,mDAAmD;AACnD,MAAM,cAAc,GAAG,CAAC,IAAyB,EAAiB,EAAE;IAClE,IAAI,IAAI,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAQ,IAAqC,CAAC,IAAI,IAAI,IAAI,CAAC;AAC7D,CAAC,CAAC;AAEF,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,oGAAoG;AACpG,MAAM,eAAe,GAAG,CAAC,IAAa,EAAW,EAAE;IACjD,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,OAAQ,IAAuC,CAAC,KAAK,KAAK,QAAQ,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,2DAA2D;AAC3D,MAAM,cAAc,GAAG,CAAC,IAAa,EAAiB,EAAE;IACtD,MAAM,GAAG,GAAI,IAAuC,CAAC,KAAK,CAAC;IAC3D,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC,CAAC;AAEF,8EAA8E;AAC9E,8BAA8B;AAC9B,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,CACzB,IAAY,EACZ,UAAkB,EACH,EAAE;IACjB,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,YAAY,IAAI,kCAAkC,CACnD,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACtC,CAAC,CAAC;AAEF,8EAA8E;AAC9E,MAAM,wBAAwB,GAAG,CAC/B,EAAW,EACX,UAAkB,EACH,EAAE;IACjB,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,kBAAkB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,CAAC,MAAe,EAA6B,EAAE;IACvE,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC;IACnC,IAAI,CAAC,SAAS,IAAK,SAAqB,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAI,SAAqB,CAAC,UAAU,CAErC,CAAC;IACd,OAAO,QAAQ,IAAI,IAAI,CAAC;AAC1B,CAAC,CAAC;AAEF,mFAAmF;AACnF,MAAM,gBAAgB,GAAG,CACvB,QAA4B,EAC5B,UAAkB,EACL,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;YAC/B,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACf,CAAC;QACH,CAAC;aAAM,IAAI,EAAE,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACpC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YAC1D,IAAI,QAAQ,EAAE,CAAC;gBACb,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,sBAAsB,GAAG,CAC7B,MAAe,EACf,UAAkB,EACG,EAAE;IACvB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;AACvE,CAAC,CAAC;AAEF,8EAA8E;AAC9E,uDAAuD;AACvD,8EAA8E;AAE9E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,wBAAwB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAE7E,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,CACxB,MAAe,EAC+B,EAAE;IAChD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAC3B,MAA0C,CAAC,MAAM,CACnD,CAAC;IACF,MAAM,QAAQ,GAAG,cAAc,CAC5B,MAA4C,CAAC,QAAQ,CACvD,CAAC;IAEF,OAAO,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC,CAAC;AAEF,gFAAgF;AAChF,MAAM,qBAAqB,GAAG,CAAC,IAAa,EAAiB,EAAE;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAmC,CAAC;IACjE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,QAAQ,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5C,yCAAyC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,uBAAuB,GAAG,CAAC,OAAgB,EAAiB,EAAE;IAClE,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAmC,CAAC;IACnE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACnC,CAAC,CAAC;AAEF,8EAA8E;AAC9E,MAAM,kBAAkB,GAAG,CACzB,MAAe,EACf,QAA6B,EACpB,EAAE;IACX,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,CAAC,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC;AAC5E,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,CAC1B,IAAa,EACb,QAA6B,EACd,EAAE;IACjB,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAwB,CAAC;IACrD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACzC,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,gDAAgD;IAChD,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;QACxC,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,iEAAiE;AACjE,MAAM,aAAa,GAAG,CAAC,IAAa,EAAuB,EAAE;IAC3D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,SAAS,EAAE,CAAC;QACd,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,oFAAoF;AACpF,MAAM,oBAAoB,GAAG,CAAC,MAAe,EAAuB,EAAE;IACpE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAE9B,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAErC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE;YAClB,MAAM,EAAE,GAAG,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC/C,IAAI,EAAE,EAAE,CAAC;gBACP,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,MAAM,yBAAyB,GAAG,CAChC,OAAe,EACf,QAAgB,EAChB,QAAgB,EAChB,IAAY,EACM,EAAE,CAAC,CAAC;IACtB,QAAQ;IACR,IAAI;IACJ,OAAO,EAAE,UAAU,OAAO,kBAAkB,QAAQ,kBAAkB,QAAQ,6BAA6B;IAC3G,IAAI,EAAE,qBAAqB;IAC3B,QAAQ,EAAE,OAAO;CAClB,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG,CAC5B,OAAe,EACf,QAAgB,EAChB,QAAgB,EAChB,IAAY,EACM,EAAE,CAAC,CAAC;IACtB,QAAQ;IACR,IAAI;IACJ,OAAO,EAAE,UAAU,OAAO,OAAO,QAAQ,wCAAwC,QAAQ,iBAAiB;IAC1G,IAAI,EAAE,qBAAqB;IAC3B,QAAQ,EAAE,MAAM;CACjB,CAAC,CAAC;AAEH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,iEAAiE;AACjE,MAAM,gBAAgB,GAAG,CACvB,MAA2B,EAC3B,QAA6B,EAC7B,GAAwD,EACxD,WAA+B,EACzB,EAAE;IACR,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACtB,WAAW,CAAC,IAAI,CACd,yBAAyB,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,CACnE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,mEAAmE;AACnE,MAAM,YAAY,GAAG,CACnB,QAA6B,EAC7B,MAA2B,EAC3B,GAAwD,EACxD,WAA+B,EACzB,EAAE;IACR,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACpB,WAAW,CAAC,IAAI,CACd,qBAAqB,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,CAC/D,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CAC3B,GAAmD,EACnD,QAAgB,EAChB,UAAkB,EAClB,WAA+B,EACzB,EAAE;IACR,MAAM,QAAQ,GAAG,sBAAsB,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAEhD,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;IAEhD,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;IACrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF,8EAA8E;AAC9E,OAAO;AACP,8EAA8E;AAE9E;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,KAAK,CAAC,UAAkB,EAAE,QAAgB;QACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,WAAW,GAAuB,EAAE,CAAC;QAE3C,KAAK,MAAM,GAAG,IAAI,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;QAC/D,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,WAAW,EACT,iFAAiF;IACnF,IAAI,EAAE,qBAAqB;IAC3B,QAAQ,EAAE,OAAO;CAClB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/warden",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"zod": "^4.3.5"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
23
|
-
"@ontrails/schema": "^1.0.0-beta.
|
|
22
|
+
"@ontrails/core": "^1.0.0-beta.6",
|
|
23
|
+
"@ontrails/schema": "^1.0.0-beta.6"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -85,6 +85,158 @@ trail('onboard', {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
describe('single-object overload', () => {
|
|
89
|
+
test('recognizes trail({ id, follow, run }) form', () => {
|
|
90
|
+
const code = `
|
|
91
|
+
trail({
|
|
92
|
+
id: 'onboard',
|
|
93
|
+
follow: ['entity.add'],
|
|
94
|
+
input: z.object({ name: z.string() }),
|
|
95
|
+
run: async (input, ctx) => {
|
|
96
|
+
await ctx.follow('entity.add', { name: input.name });
|
|
97
|
+
return Result.ok({});
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
103
|
+
|
|
104
|
+
expect(diagnostics.length).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('detects undeclared follows in single-object form', () => {
|
|
108
|
+
const code = `
|
|
109
|
+
trail({
|
|
110
|
+
id: 'onboard',
|
|
111
|
+
input: z.object({ name: z.string() }),
|
|
112
|
+
run: async (input, ctx) => {
|
|
113
|
+
await ctx.follow('entity.add', { name: input.name });
|
|
114
|
+
return Result.ok({});
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
120
|
+
|
|
121
|
+
expect(diagnostics.length).toBe(1);
|
|
122
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
123
|
+
expect(diagnostics[0]?.message).toContain("'entity.add'");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('context parameter naming', () => {
|
|
128
|
+
test('recognizes context.follow() when second param is named context', () => {
|
|
129
|
+
const code = `
|
|
130
|
+
trail('onboard', {
|
|
131
|
+
follow: ['entity.add'],
|
|
132
|
+
input: z.object({ name: z.string() }),
|
|
133
|
+
run: async (input, context) => {
|
|
134
|
+
await context.follow('entity.add', { name: input.name });
|
|
135
|
+
return Result.ok({});
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
141
|
+
|
|
142
|
+
expect(diagnostics.length).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('detects undeclared context.follow() calls', () => {
|
|
146
|
+
const code = `
|
|
147
|
+
trail('onboard', {
|
|
148
|
+
input: z.object({ name: z.string() }),
|
|
149
|
+
run: async (input, context) => {
|
|
150
|
+
await context.follow('entity.add', { name: input.name });
|
|
151
|
+
return Result.ok({});
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
157
|
+
|
|
158
|
+
expect(diagnostics.length).toBe(1);
|
|
159
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('recognizes destructured follow() calls', () => {
|
|
163
|
+
const code = `
|
|
164
|
+
trail('onboard', {
|
|
165
|
+
follow: ['entity.add'],
|
|
166
|
+
input: z.object({ name: z.string() }),
|
|
167
|
+
run: async (input, ctx) => {
|
|
168
|
+
const { follow } = ctx;
|
|
169
|
+
await follow('entity.add', { name: input.name });
|
|
170
|
+
return Result.ok({});
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
176
|
+
|
|
177
|
+
expect(diagnostics.length).toBe(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('nested run false positives', () => {
|
|
182
|
+
test('metadata.run does not trigger false positives', () => {
|
|
183
|
+
const code = `
|
|
184
|
+
trail('onboard', {
|
|
185
|
+
follow: ['entity.add'],
|
|
186
|
+
input: z.object({ name: z.string() }),
|
|
187
|
+
metadata: { run: async () => ctx.follow('phantom') },
|
|
188
|
+
run: async (input, ctx) => {
|
|
189
|
+
await ctx.follow('entity.add', { name: input.name });
|
|
190
|
+
return Result.ok({});
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
196
|
+
|
|
197
|
+
expect(diagnostics.length).toBe(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('identifier resolution in follow arrays', () => {
|
|
202
|
+
test('resolves const identifiers in follow array', () => {
|
|
203
|
+
const code = `
|
|
204
|
+
const ENTITY_ADD = 'entity.add';
|
|
205
|
+
trail('onboard', {
|
|
206
|
+
follow: [ENTITY_ADD],
|
|
207
|
+
input: z.object({ name: z.string() }),
|
|
208
|
+
run: async (input, ctx) => {
|
|
209
|
+
await ctx.follow('entity.add', { name: input.name });
|
|
210
|
+
return Result.ok({});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
216
|
+
|
|
217
|
+
expect(diagnostics.length).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('reports error when resolved identifier does not match called follow', () => {
|
|
221
|
+
const code = `
|
|
222
|
+
const ENTITY_ADD = 'entity.add';
|
|
223
|
+
trail('onboard', {
|
|
224
|
+
follow: [ENTITY_ADD],
|
|
225
|
+
input: z.object({ name: z.string() }),
|
|
226
|
+
run: async (input, ctx) => {
|
|
227
|
+
await ctx.follow('search', { name: input.name });
|
|
228
|
+
return Result.ok({});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const diagnostics = followDeclarations.check(code, TEST_FILE);
|
|
234
|
+
|
|
235
|
+
// 'search' called but not declared, 'entity.add' declared but not called
|
|
236
|
+
expect(diagnostics.length).toBe(2);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
88
240
|
describe('edge cases', () => {
|
|
89
241
|
test('dynamic follow IDs are skipped', () => {
|
|
90
242
|
const code = `
|
package/src/rules/ast.ts
CHANGED
|
@@ -74,17 +74,34 @@ export const offsetToLine = (sourceCode: string, offset: number): number => {
|
|
|
74
74
|
return line;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Config property extraction helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
82
|
+
export const findConfigProperty = (
|
|
83
|
+
config: AstNode,
|
|
84
|
+
propertyName: string
|
|
85
|
+
): AstNode | null => {
|
|
86
|
+
if (config.type !== 'ObjectExpression') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const properties = config['properties'] as readonly AstNode[] | undefined;
|
|
90
|
+
if (!properties) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
for (const prop of properties) {
|
|
94
|
+
if (prop.type === 'Property' && prop.key?.name === propertyName) {
|
|
95
|
+
return prop;
|
|
83
96
|
}
|
|
84
|
-
}
|
|
85
|
-
return
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
86
99
|
};
|
|
87
100
|
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Trail definition extraction
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
88
105
|
export interface TrailDefinition {
|
|
89
106
|
/** Trail ID string, e.g. "entity.show" */
|
|
90
107
|
readonly id: string;
|
|
@@ -97,7 +114,8 @@ export interface TrailDefinition {
|
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
/**
|
|
100
|
-
* Find all `trail("id", { ... })
|
|
117
|
+
* Find all `trail("id", { ... })`, `trail({ id: "x", ... })`, and
|
|
118
|
+
* `event("id", { ... })` call sites.
|
|
101
119
|
*
|
|
102
120
|
* Returns the trail ID, kind, and config object node for each definition.
|
|
103
121
|
*/
|
|
@@ -115,18 +133,49 @@ const getTrailCalleeName = (node: AstNode): string | null => {
|
|
|
115
133
|
return name && TRAIL_CALLEE_NAMES.has(name) ? name : null;
|
|
116
134
|
};
|
|
117
135
|
|
|
136
|
+
/** Extract args from a trail() call, handling both two-arg and single-object forms. */
|
|
118
137
|
const extractTrailArgs = (
|
|
119
138
|
node: AstNode
|
|
120
|
-
): { idArg: AstNode; configArg: AstNode } | null => {
|
|
139
|
+
): { idArg: AstNode | null; configArg: AstNode } | null => {
|
|
121
140
|
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
122
|
-
if (!args || args.length
|
|
141
|
+
if (!args || args.length === 0) {
|
|
123
142
|
return null;
|
|
124
143
|
}
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
|
|
145
|
+
const [firstArg, secondArg] = args;
|
|
146
|
+
if (!firstArg) {
|
|
127
147
|
return null;
|
|
128
148
|
}
|
|
129
|
-
|
|
149
|
+
|
|
150
|
+
// Two-arg form: trail('id', { ... })
|
|
151
|
+
if (secondArg && firstArg.type !== 'ObjectExpression') {
|
|
152
|
+
return { configArg: secondArg, idArg: firstArg };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Single-object form: trail({ id: 'x', ... })
|
|
156
|
+
return firstArg.type === 'ObjectExpression'
|
|
157
|
+
? { configArg: firstArg, idArg: null }
|
|
158
|
+
: null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Extract the string value from an `id` property inside a config ObjectExpression. */
|
|
162
|
+
const extractIdFromConfig = (config: AstNode): string | null => {
|
|
163
|
+
const idProp = findConfigProperty(config, 'id');
|
|
164
|
+
if (!idProp || !idProp.value) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const val = (idProp.value as unknown as { value?: unknown }).value;
|
|
168
|
+
return typeof val === 'string' ? val : null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const extractTrailId = (trailArgs: {
|
|
172
|
+
idArg: AstNode | null;
|
|
173
|
+
configArg: AstNode;
|
|
174
|
+
}): string | null => {
|
|
175
|
+
if (trailArgs.idArg) {
|
|
176
|
+
return (trailArgs.idArg as unknown as { value?: string }).value ?? null;
|
|
177
|
+
}
|
|
178
|
+
return extractIdFromConfig(trailArgs.configArg);
|
|
130
179
|
};
|
|
131
180
|
|
|
132
181
|
const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
@@ -140,7 +189,7 @@ const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
|
140
189
|
return null;
|
|
141
190
|
}
|
|
142
191
|
|
|
143
|
-
const trailId = (trailArgs
|
|
192
|
+
const trailId = extractTrailId(trailArgs);
|
|
144
193
|
if (!trailId) {
|
|
145
194
|
return null;
|
|
146
195
|
}
|
|
@@ -153,28 +202,6 @@ const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
|
153
202
|
};
|
|
154
203
|
};
|
|
155
204
|
|
|
156
|
-
/** Check if a node is a call to `.run()` on some object. */
|
|
157
|
-
export const isRunCall = (node: AstNode): boolean => {
|
|
158
|
-
if (node.type !== 'CallExpression') {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
const callee = node['callee'] as AstNode | undefined;
|
|
162
|
-
if (!callee) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
if (
|
|
166
|
-
callee.type !== 'StaticMemberExpression' &&
|
|
167
|
-
callee.type !== 'MemberExpression'
|
|
168
|
-
) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
const prop = (callee as unknown as { property?: AstNode }).property;
|
|
172
|
-
return (
|
|
173
|
-
prop?.type === 'Identifier' &&
|
|
174
|
-
(prop as unknown as { name: string }).name === 'run'
|
|
175
|
-
);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
205
|
export const findTrailDefinitions = (ast: AstNode): TrailDefinition[] => {
|
|
179
206
|
const definitions: TrailDefinition[] = [];
|
|
180
207
|
|
|
@@ -189,25 +216,71 @@ export const findTrailDefinitions = (ast: AstNode): TrailDefinition[] => {
|
|
|
189
216
|
};
|
|
190
217
|
|
|
191
218
|
// ---------------------------------------------------------------------------
|
|
192
|
-
//
|
|
219
|
+
// Run body extraction
|
|
193
220
|
// ---------------------------------------------------------------------------
|
|
194
221
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract top-level `run:` property values from an ObjectExpression's direct properties.
|
|
224
|
+
*
|
|
225
|
+
* Does not recurse into nested objects, so `metadata: { run: ... }` is ignored.
|
|
226
|
+
*/
|
|
227
|
+
const extractRunFromConfig = (config: AstNode): AstNode[] => {
|
|
228
|
+
const bodies: AstNode[] = [];
|
|
203
229
|
const properties = config['properties'] as readonly AstNode[] | undefined;
|
|
204
230
|
if (!properties) {
|
|
205
|
-
return
|
|
231
|
+
return bodies;
|
|
206
232
|
}
|
|
207
233
|
for (const prop of properties) {
|
|
208
|
-
if (prop.type === 'Property' && prop.key?.name ===
|
|
209
|
-
|
|
234
|
+
if (prop.type === 'Property' && prop.key?.name === 'run' && prop.value) {
|
|
235
|
+
bodies.push(prop.value);
|
|
210
236
|
}
|
|
211
237
|
}
|
|
212
|
-
return
|
|
238
|
+
return bodies;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find `run:` property values.
|
|
243
|
+
*
|
|
244
|
+
* When given an ObjectExpression (trail config), returns only its direct `run:`
|
|
245
|
+
* properties. When given a full AST, finds trail definitions first and extracts
|
|
246
|
+
* `run:` from each config — in both cases ignoring nested `run:` properties
|
|
247
|
+
* (e.g. `metadata: { run: ... }`).
|
|
248
|
+
*/
|
|
249
|
+
export const findRunBodies = (node: AstNode): AstNode[] => {
|
|
250
|
+
if (node.type === 'ObjectExpression') {
|
|
251
|
+
return extractRunFromConfig(node);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Full AST — find trail definitions and extract run from their configs
|
|
255
|
+
const bodies: AstNode[] = [];
|
|
256
|
+
for (const def of findTrailDefinitions(node)) {
|
|
257
|
+
bodies.push(...extractRunFromConfig(def.config));
|
|
258
|
+
}
|
|
259
|
+
return bodies;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Misc helpers
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/** Check if a node is a call to `.run()` on some object. */
|
|
267
|
+
export const isRunCall = (node: AstNode): boolean => {
|
|
268
|
+
if (node.type !== 'CallExpression') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
272
|
+
if (!callee) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
callee.type !== 'StaticMemberExpression' &&
|
|
277
|
+
callee.type !== 'MemberExpression'
|
|
278
|
+
) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const prop = (callee as unknown as { property?: AstNode }).property;
|
|
282
|
+
return (
|
|
283
|
+
prop?.type === 'Identifier' &&
|
|
284
|
+
(prop as unknown as { name: string }).name === 'run'
|
|
285
|
+
);
|
|
213
286
|
};
|
|
@@ -18,6 +18,18 @@ import type { AstNode } from './ast.js';
|
|
|
18
18
|
import { isTestFile } from './scan.js';
|
|
19
19
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
20
20
|
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared identifier helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Get the name of an Identifier node, or null. */
|
|
26
|
+
const identifierName = (node: AstNode | undefined): string | null => {
|
|
27
|
+
if (node?.type !== 'Identifier') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return (node as unknown as { name?: string }).name ?? null;
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
// ---------------------------------------------------------------------------
|
|
22
34
|
// String literal helpers
|
|
23
35
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +51,42 @@ const getStringValue = (node: AstNode): string | null => {
|
|
|
39
51
|
return typeof val === 'string' ? val : null;
|
|
40
52
|
};
|
|
41
53
|
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Const identifier resolution
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort resolution of `const NAME = 'value'` declarations via regex.
|
|
60
|
+
*
|
|
61
|
+
* Returns the string value if a simple `const <name> = '...'` or `"..."` is
|
|
62
|
+
* found in the source. Returns null for anything more complex.
|
|
63
|
+
*/
|
|
64
|
+
const resolveConstString = (
|
|
65
|
+
name: string,
|
|
66
|
+
sourceCode: string
|
|
67
|
+
): string | null => {
|
|
68
|
+
const pattern = new RegExp(
|
|
69
|
+
`const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`
|
|
70
|
+
);
|
|
71
|
+
const match = pattern.exec(sourceCode);
|
|
72
|
+
if (!match) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return match[1] ?? match[2] ?? null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Try to resolve an Identifier element to a string via const declaration. */
|
|
79
|
+
const resolveIdentifierElement = (
|
|
80
|
+
el: AstNode,
|
|
81
|
+
sourceCode: string
|
|
82
|
+
): string | null => {
|
|
83
|
+
const name = identifierName(el);
|
|
84
|
+
if (!name) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return resolveConstString(name, sourceCode);
|
|
88
|
+
};
|
|
89
|
+
|
|
42
90
|
// ---------------------------------------------------------------------------
|
|
43
91
|
// Declared follow extraction
|
|
44
92
|
// ---------------------------------------------------------------------------
|
|
@@ -61,8 +109,11 @@ const getFollowElements = (config: AstNode): readonly AstNode[] | null => {
|
|
|
61
109
|
return elements ?? null;
|
|
62
110
|
};
|
|
63
111
|
|
|
64
|
-
/** Collect string IDs from array elements. */
|
|
65
|
-
const collectStringIds = (
|
|
112
|
+
/** Collect string IDs from array elements, resolving identifiers when possible. */
|
|
113
|
+
const collectStringIds = (
|
|
114
|
+
elements: readonly AstNode[],
|
|
115
|
+
sourceCode: string
|
|
116
|
+
): Set<string> => {
|
|
66
117
|
const ids = new Set<string>();
|
|
67
118
|
for (const el of elements) {
|
|
68
119
|
if (isStringLiteral(el)) {
|
|
@@ -70,15 +121,23 @@ const collectStringIds = (elements: readonly AstNode[]): Set<string> => {
|
|
|
70
121
|
if (val) {
|
|
71
122
|
ids.add(val);
|
|
72
123
|
}
|
|
124
|
+
} else if (el.type === 'Identifier') {
|
|
125
|
+
const resolved = resolveIdentifierElement(el, sourceCode);
|
|
126
|
+
if (resolved) {
|
|
127
|
+
ids.add(resolved);
|
|
128
|
+
}
|
|
73
129
|
}
|
|
74
130
|
}
|
|
75
131
|
return ids;
|
|
76
132
|
};
|
|
77
133
|
|
|
78
134
|
/** Extract string literal elements from a `follow: [...]` array property. */
|
|
79
|
-
const extractDeclaredFollows = (
|
|
135
|
+
const extractDeclaredFollows = (
|
|
136
|
+
config: AstNode,
|
|
137
|
+
sourceCode: string
|
|
138
|
+
): ReadonlySet<string> => {
|
|
80
139
|
const elements = getFollowElements(config);
|
|
81
|
-
return elements ? collectStringIds(elements) : new Set();
|
|
140
|
+
return elements ? collectStringIds(elements, sourceCode) : new Set();
|
|
82
141
|
};
|
|
83
142
|
|
|
84
143
|
// ---------------------------------------------------------------------------
|
|
@@ -87,14 +146,6 @@ const extractDeclaredFollows = (config: AstNode): ReadonlySet<string> => {
|
|
|
87
146
|
|
|
88
147
|
const MEMBER_TYPES = new Set(['StaticMemberExpression', 'MemberExpression']);
|
|
89
148
|
|
|
90
|
-
/** Get the name of an Identifier node, or null. */
|
|
91
|
-
const identifierName = (node: AstNode | undefined): string | null => {
|
|
92
|
-
if (node?.type !== 'Identifier') {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return (node as unknown as { name?: string }).name ?? null;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
149
|
/** Extract object and property Identifier names from a MemberExpression. */
|
|
99
150
|
const extractMemberPair = (
|
|
100
151
|
callee: AstNode
|
|
@@ -129,8 +180,38 @@ const extractFirstStringArg = (node: AstNode): string | null => {
|
|
|
129
180
|
return getStringValue(firstArg);
|
|
130
181
|
};
|
|
131
182
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Extract the second parameter name from a run function node.
|
|
185
|
+
*
|
|
186
|
+
* Handles `(input, ctx) => ...` and `async (input, context) => ...` and
|
|
187
|
+
* `function(input, ctx) { ... }` forms.
|
|
188
|
+
*/
|
|
189
|
+
const extractContextParamName = (runBody: AstNode): string | null => {
|
|
190
|
+
const params = runBody['params'] as readonly AstNode[] | undefined;
|
|
191
|
+
if (!params || params.length < 2) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return identifierName(params[1]);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** Check if a callee is a member-style follow call: <ctxName>.follow(...). */
|
|
198
|
+
const isMemberFollowCall = (
|
|
199
|
+
callee: AstNode,
|
|
200
|
+
ctxNames: ReadonlySet<string>
|
|
201
|
+
): boolean => {
|
|
202
|
+
const pair = extractMemberPair(callee);
|
|
203
|
+
return !!pair && ctxNames.has(pair.objName) && pair.propName === 'follow';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a node is a `<ctxName>.follow(...)` call and return the string trail ID.
|
|
208
|
+
*
|
|
209
|
+
* Also matches bare `follow(...)` calls (destructured pattern).
|
|
210
|
+
*/
|
|
211
|
+
const extractFollowCallId = (
|
|
212
|
+
node: AstNode,
|
|
213
|
+
ctxNames: ReadonlySet<string>
|
|
214
|
+
): string | null => {
|
|
134
215
|
if (node.type !== 'CallExpression') {
|
|
135
216
|
return null;
|
|
136
217
|
}
|
|
@@ -140,12 +221,26 @@ const extractFollowCallId = (node: AstNode): string | null => {
|
|
|
140
221
|
return null;
|
|
141
222
|
}
|
|
142
223
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
224
|
+
if (isMemberFollowCall(callee, ctxNames)) {
|
|
225
|
+
return extractFirstStringArg(node);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Match bare follow(...) — destructured pattern
|
|
229
|
+
if (identifierName(callee) === 'follow') {
|
|
230
|
+
return extractFirstStringArg(node);
|
|
146
231
|
}
|
|
147
232
|
|
|
148
|
-
return
|
|
233
|
+
return null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Build the set of context parameter names to match against. */
|
|
237
|
+
const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
238
|
+
const ctxNames = new Set(['ctx', 'context']);
|
|
239
|
+
const paramName = extractContextParamName(body);
|
|
240
|
+
if (paramName) {
|
|
241
|
+
ctxNames.add(paramName);
|
|
242
|
+
}
|
|
243
|
+
return ctxNames;
|
|
149
244
|
};
|
|
150
245
|
|
|
151
246
|
/** Walk run bodies and collect all statically resolvable ctx.follow() trail IDs. */
|
|
@@ -153,8 +248,10 @@ const extractCalledFollows = (config: AstNode): ReadonlySet<string> => {
|
|
|
153
248
|
const ids = new Set<string>();
|
|
154
249
|
|
|
155
250
|
for (const body of findRunBodies(config)) {
|
|
251
|
+
const ctxNames = buildCtxNames(body);
|
|
252
|
+
|
|
156
253
|
walk(body, (node) => {
|
|
157
|
-
const id = extractFollowCallId(node);
|
|
254
|
+
const id = extractFollowCallId(node, ctxNames);
|
|
158
255
|
if (id) {
|
|
159
256
|
ids.add(id);
|
|
160
257
|
}
|
|
@@ -236,7 +333,7 @@ const checkTrailDefinition = (
|
|
|
236
333
|
sourceCode: string,
|
|
237
334
|
diagnostics: WardenDiagnostic[]
|
|
238
335
|
): void => {
|
|
239
|
-
const declared = extractDeclaredFollows(def.config);
|
|
336
|
+
const declared = extractDeclaredFollows(def.config, sourceCode);
|
|
240
337
|
const called = extractCalledFollows(def.config);
|
|
241
338
|
|
|
242
339
|
if (declared.size === 0 && called.size === 0) {
|