@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.
@@ -1,3 +1,3 @@
1
1
  $ oxlint ./src
2
2
  Found 0 warnings and 0 errors.
3
- Finished in 19ms on 32 files with 93 rules using 24 threads.
3
+ Finished in 25ms on 32 files with 93 rules using 24 threads.
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
@@ -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 all `run:` property values in an AST. */
25
- export declare const findRunBodies: (ast: AstNode) => AstNode[];
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
@@ -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;AAEF,iDAAiD;AACjD,eAAO,MAAM,aAAa,GAAI,KAAK,OAAO,KAAG,OAAO,EAQnD,CAAC;AAEF,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;AA2DD,4DAA4D;AAC5D,eAAO,MAAM,SAAS,GAAI,MAAM,OAAO,KAAG,OAmBzC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,KAAK,OAAO,KAAG,eAAe,EAWlE,CAAC;AAMF,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,OAAO,EACf,cAAc,MAAM,KACnB,OAAO,GAAG,IAcZ,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
- /** Find all `run:` property values in an AST. */
58
- export const findRunBodies = (ast) => {
59
- const bodies = [];
60
- walk(ast, (node) => {
61
- if (node.type === 'Property' && node.key?.name === 'run' && node.value) {
62
- bodies.push(node.value);
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 bodies;
73
+ }
74
+ return null;
66
75
  };
67
76
  /**
68
- * Find all `trail("id", { ... })` and `event("id", { ... })` call sites.
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 < 2) {
97
+ if (!args || args.length === 0) {
87
98
  return null;
88
99
  }
89
- const [idArg, configArg] = args;
90
- if (!idArg || !configArg) {
100
+ const [firstArg, secondArg] = args;
101
+ if (!firstArg) {
91
102
  return null;
92
103
  }
93
- return { configArg, idArg };
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.idArg.value;
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
- // Config property extraction helpers
159
+ // Run body extraction
144
160
  // ---------------------------------------------------------------------------
145
- /** Find a Property node by key name inside an ObjectExpression config. */
146
- export const findConfigProperty = (config, propertyName) => {
147
- if (config.type !== 'ObjectExpression') {
148
- return null;
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 null;
170
+ return bodies;
153
171
  }
154
172
  for (const prop of properties) {
155
- if (prop.type === 'Property' && prop.key?.name === propertyName) {
156
- return prop;
173
+ if (prop.type === 'Property' && prop.key?.name === 'run' && prop.value) {
174
+ bodies.push(prop.value);
157
175
  }
158
176
  }
159
- return null;
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
@@ -1 +1 @@
1
- {"version":3,"file":"ast.js","sourceRoot":"","sources":["../../src/rules/ast.ts"],"names":[],"mappings":"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"}
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,8EAA8E;AAC9E,qCAAqC;AACrC,8EAA8E;AAE9E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,MAAe,EACf,YAAoB,EACJ,EAAE;IAClB,IAAI,MAAM,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAmC,CAAC;IAC1E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;YAChE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAiBF;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvD,MAAM,kBAAkB,GAAG,CAAC,IAAa,EAAiB,EAAE;IAC1D,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAwB,CAAC;IACrD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAsC,CAAC;IACxD,OAAO,IAAI,IAAI,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC,CAAC;AAEF,uFAAuF;AACvF,MAAM,gBAAgB,GAAG,CACvB,IAAa,EACyC,EAAE;IACxD,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,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC;IACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAED,qCAAqC;IACrC,IAAI,SAAS,IAAI,QAAQ,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACtD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IACnD,CAAC;IAED,8CAA8C;IAC9C,OAAO,QAAQ,CAAC,IAAI,KAAK,kBAAkB;QACzC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE;QACtC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC,CAAC;AAEF,uFAAuF;AACvF,MAAM,mBAAmB,GAAG,CAAC,MAAe,EAAiB,EAAE;IAC7D,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAI,MAAM,CAAC,KAAwC,CAAC,KAAK,CAAC;IACnE,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,SAGvB,EAAiB,EAAE;IAClB,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;QACpB,OAAQ,SAAS,CAAC,KAAuC,CAAC,KAAK,IAAI,IAAI,CAAC;IAC1E,CAAC;IACD,OAAO,mBAAmB,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,IAAa,EAA0B,EAAE;IACvE,MAAM,UAAU,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,MAAM,EAAE,SAAS,CAAC,SAAS;QAC3B,EAAE,EAAE,OAAO;QACX,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAY,EAAqB,EAAE;IACtE,MAAM,WAAW,GAAsB,EAAE,CAAC;IAE1C,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;QACjB,MAAM,GAAG,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,GAAG,EAAE,CAAC;YACR,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,WAAW,CAAC;AACrB,CAAC,CAAC;AAEF,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,CAAC,MAAe,EAAa,EAAE;IAC1D,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAmC,CAAC;IAC1E,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACvE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAa,EAAa,EAAE;IACxD,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACrC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,uEAAuE;IACvE,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAa,EAAW,EAAE;IAClD,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAwB,CAAC;IACrD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IACE,MAAM,CAAC,IAAI,KAAK,wBAAwB;QACxC,MAAM,CAAC,IAAI,KAAK,kBAAkB,EAClC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAI,MAA4C,CAAC,QAAQ,CAAC;IACpE,OAAO,CACL,IAAI,EAAE,IAAI,KAAK,YAAY;QAC1B,IAAoC,CAAC,IAAI,KAAK,KAAK,CACrD,CAAC;AACJ,CAAC,CAAC"}
@@ -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;AA8O/D;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,UAuBhC,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
- /** Check if a node is a `ctx.follow(...)` call and return the string trail ID. */
96
- const extractFollowCallId = (node) => {
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
- const pair = extractMemberPair(callee);
105
- if (!pair || pair.objName !== 'ctx' || pair.propName !== 'follow') {
106
- return null;
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 extractFirstStringArg(node);
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":"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"}
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.5",
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.0",
23
- "@ontrails/schema": "^1.0.0-beta.0"
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
- /** Find all `run:` property values in an AST. */
78
- export const findRunBodies = (ast: AstNode): AstNode[] => {
79
- const bodies: AstNode[] = [];
80
- walk(ast, (node) => {
81
- if (node.type === 'Property' && node.key?.name === 'run' && node.value) {
82
- bodies.push(node.value);
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 bodies;
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", { ... })` and `event("id", { ... })` call sites.
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 < 2) {
141
+ if (!args || args.length === 0) {
123
142
  return null;
124
143
  }
125
- const [idArg, configArg] = args;
126
- if (!idArg || !configArg) {
144
+
145
+ const [firstArg, secondArg] = args;
146
+ if (!firstArg) {
127
147
  return null;
128
148
  }
129
- return { configArg, idArg };
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.idArg as unknown as { value?: string }).value;
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
- // Config property extraction helpers
219
+ // Run body extraction
193
220
  // ---------------------------------------------------------------------------
194
221
 
195
- /** Find a Property node by key name inside an ObjectExpression config. */
196
- export const findConfigProperty = (
197
- config: AstNode,
198
- propertyName: string
199
- ): AstNode | null => {
200
- if (config.type !== 'ObjectExpression') {
201
- return null;
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 null;
231
+ return bodies;
206
232
  }
207
233
  for (const prop of properties) {
208
- if (prop.type === 'Property' && prop.key?.name === propertyName) {
209
- return prop;
234
+ if (prop.type === 'Property' && prop.key?.name === 'run' && prop.value) {
235
+ bodies.push(prop.value);
210
236
  }
211
237
  }
212
- return null;
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 = (elements: readonly AstNode[]): Set<string> => {
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 = (config: AstNode): ReadonlySet<string> => {
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
- /** Check if a node is a `ctx.follow(...)` call and return the string trail ID. */
133
- const extractFollowCallId = (node: AstNode): string | null => {
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
- const pair = extractMemberPair(callee);
144
- if (!pair || pair.objName !== 'ctx' || pair.propName !== 'follow') {
145
- return null;
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 extractFirstStringArg(node);
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) {