@step-forge/step-forge 0.0.25-alpha.4 → 0.0.25-alpha.5

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/RUNTIME.md CHANGED
@@ -93,16 +93,16 @@ npm install --save-dev @step-forge/step-forge
93
93
  Config is resolved from three sources, later ones overriding earlier ones:
94
94
  **defaults → `step-forge.config.ts` → CLI flags**. Every field is optional.
95
95
 
96
- | Field | Type | Default | Meaning |
97
- | ------------- | ------------------------- | ------------------ | ----------------------------------------------------------------------- |
98
- | `features` | `string \| string[]` | `**/*.feature` | Feature-file glob(s), relative to the config directory. |
99
- | `steps` | `string \| string[]` | `**/*.steps.ts` | Step-module glob(s). Importing them is what registers your steps. |
100
- | `world` | `string` | `BasicWorld` | Module that default-exports a world factory `() => world`. |
101
- | `concurrency` | `number` | `1` (serial) | Max scenarios in flight at once. See [Concurrency](#concurrency). |
102
- | `reporter` | `"pretty" \| "progress"` | `pretty` | Output style. See [Reporters & output](#reporters--output). |
103
- | `verbose` | `boolean` | `false` | Report every scenario, not just failures. |
104
- | `name` | `string` | — | Only scenarios whose name matches (substring, or `/regex/flags`). |
105
- | `tags` | `string` | — | Cucumber tag expression, e.g. `@smoke and not @wip`. |
96
+ | Field | Type | Default | Meaning |
97
+ | ------------- | ------------------------ | --------------- | ----------------------------------------------------------------- |
98
+ | `features` | `string \| string[]` | `**/*.feature` | Feature-file glob(s), relative to the config directory. |
99
+ | `steps` | `string \| string[]` | `**/*.steps.ts` | Step-module glob(s). Importing them is what registers your steps. |
100
+ | `world` | `string` | `BasicWorld` | Module that default-exports a world factory `() => world`. |
101
+ | `concurrency` | `number` | `1` (serial) | Max scenarios in flight at once. See [Concurrency](#concurrency). |
102
+ | `reporter` | `"pretty" \| "progress"` | `pretty` | Output style. See [Reporters & output](#reporters--output). |
103
+ | `verbose` | `boolean` | `false` | Report every scenario, not just failures. |
104
+ | `name` | `string` | — | Only scenarios whose name matches (substring, or `/regex/flags`). |
105
+ | `tags` | `string` | — | Cucumber tag expression, e.g. `@smoke and not @wip`. |
106
106
 
107
107
  The config file is loaded by Bun, so it may be TypeScript and import the
108
108
  `RunnerOptions` type for editor help.
@@ -116,18 +116,18 @@ step-forge [options] [feature globs...]
116
116
  Positional arguments are feature globs and **override** the configured
117
117
  `features`, so you can run a subset ad hoc.
118
118
 
119
- | Flag | Short | Description |
120
- | ----------------------- | ----- | ------------------------------------------------------- |
121
- | `--tags <expr>` | `-t` | Tag expression, e.g. `"@smoke and not @wip"`. |
122
- | `--name <pattern>` | `-n` | Only scenarios whose name matches (substring/`/regex/`).|
123
- | `--steps <glob>` | `-s` | Step-module glob (repeatable). |
124
- | `--world <module>` | `-w` | World factory module. |
125
- | `--concurrency <n>` | `-c` | Max scenarios in flight (default `1`). |
126
- | `--reporter <name>` | `-r` | `pretty` (default) or `progress`. |
127
- | `--verbose` | `-v` | Report every scenario, not just failures. |
128
- | `--interactive` | `-i` | Interactive watch mode (see below). Requires a TTY. |
129
- | `--config <path>` | | Directory to resolve the config file and globs from. |
130
- | `--help` | `-h` | Show usage. |
119
+ | Flag | Short | Description |
120
+ | ------------------- | ----- | -------------------------------------------------------- |
121
+ | `--tags <expr>` | `-t` | Tag expression, e.g. `"@smoke and not @wip"`. |
122
+ | `--name <pattern>` | `-n` | Only scenarios whose name matches (substring/`/regex/`). |
123
+ | `--steps <glob>` | `-s` | Step-module glob (repeatable). |
124
+ | `--world <module>` | `-w` | World factory module. |
125
+ | `--concurrency <n>` | `-c` | Max scenarios in flight (default `1`). |
126
+ | `--reporter <name>` | `-r` | `pretty` (default) or `progress`. |
127
+ | `--verbose` | `-v` | Report every scenario, not just failures. |
128
+ | `--interactive` | `-i` | Interactive watch mode (see below). Requires a TTY. |
129
+ | `--config <path>` | | Directory to resolve the config file and globs from. |
130
+ | `--help` | `-h` | Show usage. |
131
131
 
132
132
  Examples:
133
133
 
@@ -159,7 +159,7 @@ step-forge -i # browse and pick from everything
159
159
  step-forge -i -t "@smoke" # open with the query pre-filled
160
160
  ```
161
161
 
162
- The prompt is always live, with one committed selection (the *armed*
162
+ The prompt is always live, with one committed selection (the _armed_
163
163
  population):
164
164
 
165
165
  - **Enter** arms the highlighted choice, runs it immediately, and re-runs it on
@@ -206,11 +206,11 @@ beforeScenario(({ world, scenario }) => {
206
206
  });
207
207
  ```
208
208
 
209
- | Hook | Runs |
210
- | --------------------- | ------------------------------------------------ |
211
- | `beforeAll`/`afterAll` | Once around the entire run — `beforeAll` before concurrency starts, `afterAll` after every scenario is done. |
212
- | `beforeFeature`/`afterFeature` | Around each feature file. |
213
- | `beforeScenario`/`afterScenario` | Around each scenario (gets its world).|
209
+ | Hook | Runs |
210
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
211
+ | `beforeAll`/`afterAll` | Once around the entire run — `beforeAll` before concurrency starts, `afterAll` after every scenario is done. |
212
+ | `beforeFeature`/`afterFeature` | Around each feature file. |
213
+ | `beforeScenario`/`afterScenario` | Around each scenario (gets its world). |
214
214
 
215
215
  Multiple `beforeAll` (and multiple `afterAll`) hooks run **in parallel** with no
216
216
  ordering between them — if one setup step must precede another, sequence both
@@ -4,7 +4,6 @@ import ts from "typescript";
4
4
  import * as fs from "node:fs";
5
5
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
6
6
  import * as messages from "@cucumber/messages";
7
- import { CucumberExpression, ParameterType, ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
8
7
  //#region src/globFiles.ts
9
8
  /** Glob metacharacters. A pattern with none of these is a literal path. */
10
9
  const MAGIC = /[*?[\]{}!()]/;
@@ -44,21 +43,6 @@ const BUILDER_NAMES = {
44
43
  whenBuilder: "when",
45
44
  thenBuilder: "then"
46
45
  };
47
- /**
48
- * Built-in parsers exported by the library, mapped to the cucumber-expression
49
- * placeholder they register. A step's `.parsers([...])` drives which `{name}`
50
- * each variable becomes; without this the extractor defaulted every variable to
51
- * `{string}`, so `the deposit amount is {int}` was recorded as `{string}` and a
52
- * plain number like `100` looked unmatched. Custom parsers are resolved from
53
- * their `name` property (see {@link resolveCustomParserName}); anything we can't
54
- * resolve falls back to `string`.
55
- */
56
- const BUILTIN_PARSER_PLACEHOLDERS = {
57
- intParser: "int",
58
- numberParser: "float",
59
- stringParser: "string",
60
- booleanParser: "boolean"
61
- };
62
46
  function extractStepDefinitions(filePaths, tsConfigPath) {
63
47
  const fast = extractWithSources(filePaths.map(parseSourceFile).filter((s) => s !== null), null);
64
48
  if (!fast.needsChecker) return fast.results;
@@ -117,7 +101,6 @@ function extractFromSourceFile(sourceFile, ctx) {
117
101
  function extractFromRegisterCall(registerCall, sourceFile, ctx) {
118
102
  const chain = collectCallChain(registerCall);
119
103
  let stepType = null;
120
- let statementCall = null;
121
104
  let expression = null;
122
105
  let dependencies = {
123
106
  given: {},
@@ -125,7 +108,6 @@ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
125
108
  then: {}
126
109
  };
127
110
  let produces = [];
128
- let placeholders = null;
129
111
  for (const link of chain) {
130
112
  const name = getCallName(link);
131
113
  if (!name) continue;
@@ -138,12 +120,8 @@ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
138
120
  dependencies = extractDependencies(link);
139
121
  continue;
140
122
  }
141
- if (name === "parsers") {
142
- placeholders = resolveParserPlaceholders(link, sourceFile);
143
- continue;
144
- }
145
123
  if (name === "statement") {
146
- statementCall = link;
124
+ expression = extractExpression(link);
147
125
  continue;
148
126
  }
149
127
  if (BUILDER_NAMES[name]) {
@@ -151,7 +129,6 @@ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
151
129
  continue;
152
130
  }
153
131
  }
154
- if (statementCall) expression = extractExpression(statementCall, placeholders);
155
132
  if (!stepType || !expression) {
156
133
  const reExport = resolveReExportedCall(chain, ctx);
157
134
  if (reExport) {
@@ -176,12 +153,11 @@ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
176
153
  function collectCallChain(call) {
177
154
  const chain = [call];
178
155
  let current = call.expression;
179
- while (true) {
156
+ if (ts.isPropertyAccessExpression(current)) current = current.expression;
157
+ while (ts.isCallExpression(current)) {
158
+ chain.push(current);
159
+ current = current.expression;
180
160
  if (ts.isPropertyAccessExpression(current)) current = current.expression;
181
- if (ts.isCallExpression(current)) {
182
- chain.push(current);
183
- current = current.expression;
184
- } else break;
185
161
  }
186
162
  return chain;
187
163
  }
@@ -191,14 +167,14 @@ function getCallName(call) {
191
167
  if (ts.isIdentifier(expr)) return expr.text;
192
168
  return null;
193
169
  }
194
- function extractExpression(statementCall, placeholders) {
170
+ function extractExpression(statementCall) {
195
171
  const arg = statementCall.arguments[0];
196
172
  if (!arg) return null;
197
173
  if (ts.isStringLiteral(arg)) return arg.text;
198
- if (ts.isArrowFunction(arg)) return extractExpressionFromArrowFunction(arg, placeholders);
174
+ if (ts.isArrowFunction(arg)) return extractExpressionFromArrowFunction(arg);
199
175
  return null;
200
176
  }
201
- function extractExpressionFromArrowFunction(fn, placeholders) {
177
+ function extractExpressionFromArrowFunction(fn) {
202
178
  const params = fn.parameters.map((p) => p.name.getText());
203
179
  let body = fn.body;
204
180
  if (ts.isBlock(body)) {
@@ -206,63 +182,19 @@ function extractExpressionFromArrowFunction(fn, placeholders) {
206
182
  if (returnStmt?.expression) body = returnStmt.expression;
207
183
  else return null;
208
184
  }
209
- if (ts.isTemplateExpression(body)) return reconstructExpressionFromTemplate(body, params, placeholders);
185
+ if (ts.isTemplateExpression(body)) return reconstructExpressionFromTemplate(body, params);
210
186
  if (ts.isNoSubstitutionTemplateLiteral(body)) return body.text;
211
187
  return null;
212
188
  }
213
- function reconstructExpressionFromTemplate(template, paramNames, placeholders) {
189
+ function reconstructExpressionFromTemplate(template, paramNames) {
214
190
  let result = template.head.text;
215
191
  for (const span of template.templateSpans) {
216
- let placeholder = "string";
217
- if (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) {
218
- const index = paramNames.indexOf(span.expression.text);
219
- placeholder = placeholders?.[index] ?? "string";
220
- }
221
- result += `{${placeholder}}`;
192
+ if (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) result += "{string}";
193
+ else result += "{string}";
222
194
  result += span.literal.text;
223
195
  }
224
196
  return result;
225
197
  }
226
- /**
227
- * Resolve `.parsers([intParser, colorParser])` to the placeholder name for each
228
- * variable, positionally. Built-ins map via {@link BUILTIN_PARSER_PLACEHOLDERS};
229
- * a custom parser is resolved from its `name` property in the same source file;
230
- * anything unresolved defaults to `string`, matching the runtime default parser.
231
- */
232
- function resolveParserPlaceholders(parsersCall, sourceFile) {
233
- const arg = parsersCall.arguments[0];
234
- if (!arg || !ts.isArrayLiteralExpression(arg)) return null;
235
- return arg.elements.map((element) => {
236
- if (ts.isIdentifier(element)) {
237
- const builtin = BUILTIN_PARSER_PLACEHOLDERS[element.text];
238
- if (builtin) return builtin;
239
- const custom = resolveCustomParserName(element.text, sourceFile);
240
- if (custom) return custom;
241
- }
242
- return "string";
243
- });
244
- }
245
- /**
246
- * Find `const <identifier> = { name: "...", ... }` in `sourceFile` and return
247
- * its `name` — the cucumber placeholder a custom `Parser` registers. Scoped to
248
- * the file, so a parser imported from elsewhere won't resolve (it falls back to
249
- * `string`); the common in-file `const colorParser: Parser<Color> = {...}` does.
250
- */
251
- function resolveCustomParserName(identifier, sourceFile) {
252
- let found = null;
253
- const visit = (node) => {
254
- if (found) return;
255
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
256
- for (const prop of node.initializer.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "name" && ts.isStringLiteralLike(prop.initializer)) {
257
- found = prop.initializer.text;
258
- return;
259
- }
260
- }
261
- ts.forEachChild(node, visit);
262
- };
263
- visit(sourceFile);
264
- return found;
265
- }
266
198
  function extractDependencies(depsCall) {
267
199
  const deps = {
268
200
  given: {},
@@ -336,7 +268,7 @@ function resolveReExportedCall(chain, ctx) {
336
268
  if (ts.isCallExpression(callExpr)) {
337
269
  const callee = callExpr.expression;
338
270
  if (ts.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {
339
- const expression = extractExpression(lastCall, null);
271
+ const expression = extractExpression(lastCall);
340
272
  return {
341
273
  stepType: BUILDER_NAMES[callee.text],
342
274
  expression
@@ -468,36 +400,13 @@ function substituteExampleValues(text, substitution) {
468
400
  }
469
401
  //#endregion
470
402
  //#region src/analyzer/stepMatcher.ts
471
- /** `{name}` tokens in a cucumber expression (empty name is the anonymous `{}`). */
472
- const PARAM_TOKEN = /\{([^}]*)\}/g;
473
- /**
474
- * Compile each definition's cucumber expression with the **same** engine the
475
- * runtime uses (`@cucumber/cucumber-expressions`), so the analyzer and the
476
- * runtime agree on what a step matches.
477
- *
478
- * A hand-rolled `{param}` → `(.+)` regex used to stand in here, but it treated
479
- * cucumber-expression alternative (`a/b`) and optional (`text(s)`) syntax as
480
- * literal characters — so any step definition using them (e.g.
481
- * `there should be {int} error/errors`) never matched, and every such step was
482
- * wrongly reported as an undefined step regardless of the real state.
483
- *
484
- * Built-in placeholders (`{int}`, `{float}`, `{string}`, `{word}`) keep their
485
- * real, strict regexes so the analyzer disambiguates the way the runtime does
486
- * (e.g. `no` isn't an `{int}`). The analyzer has only the expression string, not
487
- * a custom parser's regexp, so each custom `{name}` is registered as a permissive
488
- * parameter type (any value) — lenient about the value's exact shape, but still
489
- * anchored by the surrounding literal text.
490
- */
491
403
  function compileDefinitions(definitions) {
492
404
  const compiled = [];
493
405
  for (const def of definitions) try {
494
- const registry = new ParameterTypeRegistry();
495
- for (const [, name] of def.expression.matchAll(PARAM_TOKEN)) {
496
- if (!name || registry.lookupByTypeName(name)) continue;
497
- registry.defineParameterType(new ParameterType(name, /.+/, null, (value) => value));
498
- }
406
+ const placeholder = "###PLACEHOLDER###";
407
+ const regexStr = def.expression.replace(/\{[^}]+\}/g, placeholder).replace(/[.*+?^$()|[\]\\]/g, "\\$&").replace(new RegExp(placeholder, "g"), "(.+)");
499
408
  compiled.push({
500
- expression: new CucumberExpression(def.expression, registry),
409
+ regex: new RegExp(`^${regexStr}$`, "i"),
501
410
  definition: def
502
411
  });
503
412
  } catch {}
@@ -523,13 +432,13 @@ function findMatches(text, effectiveKeyword, compiled) {
523
432
  Then: "then"
524
433
  }[effectiveKeyword];
525
434
  const typedMatches = [];
526
- for (const { expression, definition } of compiled) {
435
+ for (const { regex, definition } of compiled) {
527
436
  if (definition.stepType !== expectedStepType) continue;
528
- if (expression.match(text) != null) typedMatches.push(definition);
437
+ if (regex.test(text)) typedMatches.push(definition);
529
438
  }
530
439
  if (typedMatches.length > 0) return typedMatches;
531
440
  const fallbackMatches = [];
532
- for (const { expression, definition } of compiled) if (expression.match(text) != null) fallbackMatches.push(definition);
441
+ for (const { regex, definition } of compiled) if (regex.test(text)) fallbackMatches.push(definition);
533
442
  return fallbackMatches;
534
443
  }
535
444
  //#endregion
@@ -640,4 +549,4 @@ async function analyze(config, options) {
640
549
  //#endregion
641
550
  export { parseFeatureContent as a, matchScenarioSteps as i, defaultRules as n, parseFeatureFiles as o, findMatchingDefinitions as r, extractStepDefinitions as s, analyze as t };
642
551
 
643
- //# sourceMappingURL=analyzer-5H699Eg4.js.map
552
+ //# sourceMappingURL=analyzer-E3Kq7Eul.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer-E3Kq7Eul.js","names":[],"sources":["../../src/globFiles.ts","../../src/analyzer/stepExtractor.ts","../../src/analyzer/gherkinParser.ts","../../src/analyzer/stepMatcher.ts","../../src/analyzer/rules/ambiguousStepRule.ts","../../src/analyzer/rules/dependencyRule.ts","../../src/analyzer/rules/undefinedStepRule.ts","../../src/analyzer/rules/index.ts","../../src/analyzer/index.ts"],"sourcesContent":["import { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import ts from \"typescript\";\nimport { StepDefinitionMeta } from \"./types.js\";\n\ntype StepType = \"given\" | \"when\" | \"then\";\n\nconst BUILDER_NAMES: Record<string, StepType> = {\n givenBuilder: \"given\",\n whenBuilder: \"when\",\n thenBuilder: \"then\",\n};\n\n/**\n * Threaded through the AST walk. `checker` is `null` on the parse-only fast\n * path; a step that genuinely needs type information sets `needsChecker`, which\n * triggers a one-time retry with a real type-checked `Program`.\n */\ninterface ExtractCtx {\n checker: ts.TypeChecker | null;\n needsChecker: boolean;\n}\n\nexport function extractStepDefinitions(\n filePaths: string[],\n tsConfigPath?: string\n): StepDefinitionMeta[] {\n // Fast path: parse each file with `createSourceFile` (tokenize + parse only,\n // no type resolution — ~1ms/file) and walk the AST. Building a full\n // type-checked `Program` loads `lib.*.d.ts` and resolves every import (~150ms)\n // and is only needed for two uncommon shapes: a `.step()` whose return isn't a\n // plain object literal, or the re-exported-builder pattern. Those set\n // `needsChecker`, and we retry once with a real Program below.\n const sources = filePaths\n .map(parseSourceFile)\n .filter((s): s is ts.SourceFile => s !== null);\n const fast = extractWithSources(sources, null);\n if (!fast.needsChecker) return fast.results;\n\n // Slow path: some step needs type information. Build the program once and\n // re-extract from *its* source files — the checker only understands nodes it\n // bound itself, so the parse-only ASTs above can't be reused here.\n const program = ts.createProgram(\n filePaths,\n resolveCompilerOptions(tsConfigPath)\n );\n const checker = program.getTypeChecker();\n const checkedSources = filePaths\n .map(fp => program.getSourceFile(fp))\n .filter((s): s is ts.SourceFile => s !== undefined);\n return extractWithSources(checkedSources, checker).results;\n}\n\n/** Parse a single file into an AST with no type resolution. */\nfunction parseSourceFile(filePath: string): ts.SourceFile | null {\n const text = ts.sys.readFile(filePath);\n if (text === undefined) return null;\n const scriptKind = filePath.endsWith(\".tsx\")\n ? ts.ScriptKind.TSX\n : ts.ScriptKind.TS;\n // setParentNodes: true — the extractor relies on `.getText()`/`.getStart()`,\n // which walk parent pointers up to the SourceFile.\n return ts.createSourceFile(\n filePath,\n text,\n ts.ScriptTarget.ESNext,\n true,\n scriptKind\n );\n}\n\n/** Resolve compiler options from tsconfig (fallback to sane defaults). */\nfunction resolveCompilerOptions(tsConfigPath?: string): ts.CompilerOptions {\n const configPath =\n tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);\n let compilerOptions: ts.CompilerOptions = {\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n moduleResolution: ts.ModuleResolutionKind.Node10,\n strict: true,\n esModuleInterop: true,\n };\n\n if (configPath) {\n const configFile = ts.readConfigFile(configPath, ts.sys.readFile);\n if (!configFile.error) {\n const parsed = ts.parseJsonConfigFileContent(\n configFile.config,\n ts.sys,\n configPath.replace(/[/\\\\][^/\\\\]+$/, \"\")\n );\n compilerOptions = parsed.options;\n }\n }\n\n // Ensure noEmit so we don't write files\n compilerOptions.noEmit = true;\n return compilerOptions;\n}\n\nfunction extractWithSources(\n sources: ts.SourceFile[],\n checker: ts.TypeChecker | null\n): { results: StepDefinitionMeta[]; needsChecker: boolean } {\n const ctx: ExtractCtx = { checker, needsChecker: false };\n const results: StepDefinitionMeta[] = [];\n for (const sourceFile of sources) {\n results.push(...extractFromSourceFile(sourceFile, ctx));\n }\n return { results, needsChecker: ctx.needsChecker };\n}\n\nfunction extractFromSourceFile(\n sourceFile: ts.SourceFile,\n ctx: ExtractCtx\n): StepDefinitionMeta[] {\n const results: StepDefinitionMeta[] = [];\n\n function visit(node: ts.Node) {\n // The chain now terminates at `.step(...)`, which is the registration\n // point (there is no `.register()` anymore).\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === \"step\"\n ) {\n const meta = extractFromRegisterCall(node, sourceFile, ctx);\n if (meta) {\n results.push(meta);\n }\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return results;\n}\n\nfunction extractFromRegisterCall(\n registerCall: ts.CallExpression,\n sourceFile: ts.SourceFile,\n ctx: ExtractCtx\n): StepDefinitionMeta | null {\n // Walk backwards through the method chain to find all parts\n // Pattern: builder().statement(...).dependencies?(...).step(...).register()\n // Or: Variable(\"...\").dependencies?(...).step(...).register()\n\n const chain = collectCallChain(registerCall);\n\n let stepType: StepType | null = null;\n let expression: string | null = null;\n let dependencies: StepDefinitionMeta[\"dependencies\"] = {\n given: {},\n when: {},\n then: {},\n };\n let produces: string[] = [];\n\n for (const link of chain) {\n const name = getCallName(link);\n if (!name) continue;\n\n if (name === \"register\") {\n // Already at register, continue\n continue;\n }\n\n if (name === \"step\") {\n produces = extractProducedKeys(link, ctx);\n continue;\n }\n\n if (name === \"dependencies\") {\n dependencies = extractDependencies(link);\n continue;\n }\n\n if (name === \"statement\") {\n expression = extractExpression(link);\n // Try to find the builder type by continuing up the chain\n continue;\n }\n\n // Check if this is a builder call like givenBuilder()\n if (BUILDER_NAMES[name]) {\n stepType = BUILDER_NAMES[name];\n continue;\n }\n }\n\n // If we didn't find the builder or expression in the chain, try the \"re-exported\" pattern:\n // const Given = givenBuilder<T>().statement;\n // Given(\"foo\").step(...).register()\n if (!stepType || !expression) {\n const reExport = resolveReExportedCall(chain, ctx);\n if (reExport) {\n if (!stepType) stepType = reExport.stepType;\n if (!expression) expression = reExport.expression;\n }\n }\n\n if (!stepType || !expression) {\n return null;\n }\n\n const line =\n sourceFile.getLineAndCharacterOfPosition(registerCall.getStart()).line + 1;\n\n return {\n stepType,\n expression,\n dependencies,\n produces,\n sourceFile: sourceFile.fileName,\n line,\n };\n}\n\n/**\n * Collect all call expressions in the method chain, from register() back to the origin.\n */\nfunction collectCallChain(call: ts.CallExpression): ts.CallExpression[] {\n const chain: ts.CallExpression[] = [call];\n let current: ts.Expression = call.expression;\n\n // Walk through PropertyAccessExpression to find the next call\n if (ts.isPropertyAccessExpression(current)) {\n current = current.expression;\n }\n\n while (ts.isCallExpression(current)) {\n chain.push(current);\n current = current.expression;\n if (ts.isPropertyAccessExpression(current)) {\n current = current.expression;\n }\n }\n\n return chain;\n}\n\nfunction getCallName(call: ts.CallExpression): string | null {\n const expr = call.expression;\n\n // .method() pattern\n if (ts.isPropertyAccessExpression(expr)) {\n return expr.name.text;\n }\n\n // direct call: functionName()\n if (ts.isIdentifier(expr)) {\n return expr.text;\n }\n\n return null;\n}\n\nfunction extractExpression(statementCall: ts.CallExpression): string | null {\n const arg = statementCall.arguments[0];\n if (!arg) return null;\n\n // String literal: .statement(\"a user\")\n if (ts.isStringLiteral(arg)) {\n return arg.text;\n }\n\n // Arrow function with template literal: .statement((name: string) => `a user named ${name}`)\n if (ts.isArrowFunction(arg)) {\n return extractExpressionFromArrowFunction(arg);\n }\n\n return null;\n}\n\nfunction extractExpressionFromArrowFunction(\n fn: ts.ArrowFunction\n): string | null {\n const params = fn.parameters.map(p => p.name.getText());\n\n // The body should be a template expression or string literal\n let body = fn.body;\n\n // If wrapped in a block with a return, unwrap\n if (ts.isBlock(body)) {\n const returnStmt = body.statements.find(ts.isReturnStatement);\n if (returnStmt?.expression) {\n body = returnStmt.expression;\n } else {\n return null;\n }\n }\n\n if (ts.isTemplateExpression(body)) {\n return reconstructExpressionFromTemplate(body, params);\n }\n\n if (ts.isNoSubstitutionTemplateLiteral(body)) {\n return body.text;\n }\n\n return null;\n}\n\nfunction reconstructExpressionFromTemplate(\n template: ts.TemplateExpression,\n paramNames: string[]\n): string {\n let result = template.head.text;\n\n for (const span of template.templateSpans) {\n if (\n ts.isIdentifier(span.expression) &&\n paramNames.includes(span.expression.text)\n ) {\n result += \"{string}\";\n } else {\n // Non-parameter expression, use {string} as fallback\n result += \"{string}\";\n }\n result += span.literal.text;\n }\n\n return result;\n}\n\nfunction extractDependencies(\n depsCall: ts.CallExpression\n): StepDefinitionMeta[\"dependencies\"] {\n const deps: StepDefinitionMeta[\"dependencies\"] = {\n given: {},\n when: {},\n then: {},\n };\n\n const arg = depsCall.arguments[0];\n if (!arg || !ts.isObjectLiteralExpression(arg)) return deps;\n\n for (const prop of arg.properties) {\n if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;\n\n const phase = prop.name.text as \"given\" | \"when\" | \"then\";\n if (!deps[phase]) continue;\n\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n for (const innerProp of prop.initializer.properties) {\n if (\n ts.isPropertyAssignment(innerProp) &&\n ts.isIdentifier(innerProp.name) &&\n ts.isStringLiteral(innerProp.initializer)\n ) {\n const val = innerProp.initializer.text;\n if (val === \"required\" || val === \"optional\") {\n deps[phase][innerProp.name.text] = val;\n }\n }\n }\n }\n }\n\n return deps;\n}\n\nfunction extractProducedKeys(\n stepCall: ts.CallExpression,\n ctx: ExtractCtx\n): string[] {\n const callback = stepCall.arguments[0];\n if (!callback) return [];\n\n // Try to get the return type of the callback by analyzing its body\n if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) {\n return extractProducedKeysFromCallback(callback, ctx);\n }\n\n return [];\n}\n\nfunction extractProducedKeysFromCallback(\n callback: ts.ArrowFunction | ts.FunctionExpression,\n ctx: ExtractCtx\n): string[] {\n const body = callback.body;\n\n // Concise arrow: () => ({ key: value })\n if (!ts.isBlock(body)) {\n return extractKeysFromExpression(body, ctx);\n }\n\n // Block body: look at return statements\n const keys = new Set<string>();\n function visitReturn(node: ts.Node) {\n if (ts.isReturnStatement(node) && node.expression) {\n for (const key of extractKeysFromExpression(node.expression, ctx)) {\n keys.add(key);\n }\n }\n ts.forEachChild(node, visitReturn);\n }\n visitReturn(body);\n return [...keys];\n}\n\nfunction extractKeysFromExpression(\n expr: ts.Expression,\n ctx: ExtractCtx\n): string[] {\n // Unwrap parenthesized expressions\n while (ts.isParenthesizedExpression(expr)) {\n expr = expr.expression;\n }\n\n // Object literal: { user: ..., token: ... }\n if (ts.isObjectLiteralExpression(expr)) {\n return expr.properties\n .filter(\n (p): p is ts.PropertyAssignment | ts.ShorthandPropertyAssignment =>\n ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)\n )\n .map(p => p.name.getText())\n .filter(Boolean);\n }\n\n // Non-literal return (a variable, a spread-only object, a function call): the\n // keys can only come from the type checker. On the parse-only pass we have\n // none — flag it so the caller retries with a real Program.\n if (!ctx.checker) {\n ctx.needsChecker = true;\n return [];\n }\n try {\n const type = ctx.checker.getTypeAtLocation(expr);\n return type\n .getProperties()\n .map(p => p.name)\n .filter(n => n !== \"merge\");\n } catch {\n return [];\n }\n}\n\ninterface ReExportResult {\n stepType: StepType;\n expression: string | null;\n}\n\nfunction resolveReExportedCall(\n chain: ts.CallExpression[],\n ctx: ExtractCtx\n): ReExportResult | null {\n // Look for the pattern: Variable(\"...\")... where Variable was assigned from builderType().statement\n // The last call in the chain (furthest from register) should be the variable call\n\n const lastCall = chain[chain.length - 1];\n if (!lastCall) return null;\n\n const expr = lastCall.expression;\n\n // If it's an identifier (like \"Given\", \"When\", \"Then\"), trace its declaration\n let identifier: ts.Identifier | null = null;\n if (ts.isIdentifier(expr)) {\n identifier = expr;\n } else if (\n ts.isPropertyAccessExpression(expr) &&\n ts.isIdentifier(expr.expression)\n ) {\n identifier = expr.expression;\n }\n\n if (!identifier) return null;\n\n // Tracing the re-exported builder's declaration needs symbol resolution,\n // which only a real Program provides. Flag for the checked retry.\n if (!ctx.checker) {\n ctx.needsChecker = true;\n return null;\n }\n\n const symbol = ctx.checker.getSymbolAtLocation(identifier);\n if (!symbol) return null;\n\n const decl = symbol.valueDeclaration;\n if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer)\n return null;\n\n // Check if initializer is builderType<T>().statement\n const init = decl.initializer;\n\n // Pattern: givenBuilder<T>().statement (PropertyAccessExpression)\n if (ts.isPropertyAccessExpression(init) && init.name.text === \"statement\") {\n const callExpr = init.expression;\n if (ts.isCallExpression(callExpr)) {\n const callee = callExpr.expression;\n if (ts.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {\n // The lastCall IS the statement call — extract expression from it\n const expression = extractExpression(lastCall);\n return {\n stepType: BUILDER_NAMES[callee.text],\n expression,\n };\n }\n }\n }\n\n return null;\n}\n","import * as fs from \"node:fs\";\nimport {\n GherkinClassicTokenMatcher,\n Parser,\n AstBuilder,\n} from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\n/**\n * One parsed feature file: its `Feature:` title (empty if unnamed) plus every\n * scenario (outline rows expanded). {@link parseFeatureContent} exposes just the\n * scenarios; {@link parseFeatureCatalog} keeps the title too, for UIs that label\n * scenarios by their feature.\n */\nexport interface ParsedFeature {\n file: string;\n name: string;\n scenarios: ParsedScenario[];\n}\n\n/** Build a fresh gherkin parser and parse `content` into a document. */\nfunction parseDocument(content: string): messages.GherkinDocument {\n const builder = new AstBuilder(messages.IdGenerator.uuid());\n const matcher = new GherkinClassicTokenMatcher();\n return new Parser(builder, matcher).parse(content);\n}\n\n/**\n * Parse feature files into a catalog of `{ file, name, scenarios }`, one entry\n * per file (one gherkin parse each). Used by interactive mode to offer\n * feature/scenario names as typeahead choices labelled by their `Feature:` title.\n */\nexport function parseFeatureCatalog(filePaths: string[]): ParsedFeature[] {\n return filePaths.map(file => {\n const document = parseDocument(fs.readFileSync(file, \"utf-8\"));\n return {\n file,\n name: document.feature?.name ?? \"\",\n scenarios: documentToScenarios(document, file),\n };\n });\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n return documentToScenarios(parseDocument(content), filePath);\n}\n\n/** Expand a parsed gherkin document into scenarios (outline rows included). */\nfunction documentToScenarios(\n gherkinDocument: messages.GherkinDocument,\n filePath: string\n): ParsedScenario[] {\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some(e => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n line: scenario.location.line,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map(c => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map(c => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map(step => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([\n ...bgParsed,\n ...scenarioSteps,\n ]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n line: row.location.line,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map(step => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map(step => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n","import { MatchedStep, ParsedScenario, StepDefinitionMeta } from \"./types.js\";\n\ninterface CompiledPattern {\n regex: RegExp;\n definition: StepDefinitionMeta;\n}\n\nfunction compileDefinitions(\n definitions: StepDefinitionMeta[]\n): CompiledPattern[] {\n const compiled: CompiledPattern[] = [];\n for (const def of definitions) {\n try {\n // Replace {paramType} placeholders with (.+) to match any value,\n // regardless of which parser placeholder ({string}, {int}, ...) the\n // step definition declared.\n const placeholder = \"###PLACEHOLDER###\";\n const regexStr = def.expression\n .replace(/\\{[^}]+\\}/g, placeholder)\n .replace(/[.*+?^$()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(new RegExp(placeholder, \"g\"), \"(.+)\");\n compiled.push({\n regex: new RegExp(`^${regexStr}$`, \"i\"),\n definition: def,\n });\n } catch {\n // Skip definitions with invalid expressions\n }\n }\n return compiled;\n}\n\nexport function matchScenarioSteps(\n scenario: ParsedScenario,\n definitions: StepDefinitionMeta[]\n): MatchedStep[] {\n const compiled = compileDefinitions(definitions);\n\n return scenario.steps.map(step => {\n const matches = findMatches(step.text, step.effectiveKeyword, compiled);\n return {\n ...step,\n definitions: matches,\n };\n });\n}\n\nexport function findMatchingDefinitions(\n text: string,\n effectiveKeyword: \"Given\" | \"When\" | \"Then\",\n definitions: StepDefinitionMeta[]\n): StepDefinitionMeta[] {\n const compiled = compileDefinitions(definitions);\n return findMatches(text, effectiveKeyword, compiled);\n}\n\nfunction findMatches(\n text: string,\n effectiveKeyword: \"Given\" | \"When\" | \"Then\",\n compiled: CompiledPattern[]\n): StepDefinitionMeta[] {\n const keywordToStepType: Record<string, string> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n };\n const expectedStepType = keywordToStepType[effectiveKeyword];\n\n // First try to match with the correct step type\n const typedMatches: StepDefinitionMeta[] = [];\n for (const { regex, definition } of compiled) {\n if (definition.stepType !== expectedStepType) continue;\n if (regex.test(text)) typedMatches.push(definition);\n }\n\n if (typedMatches.length > 0) return typedMatches;\n\n // Fallback: match any step type\n const fallbackMatches: StepDefinitionMeta[] = [];\n for (const { regex, definition } of compiled) {\n if (regex.test(text)) fallbackMatches.push(definition);\n }\n\n return fallbackMatches;\n}\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const ambiguousStepRule: AnalysisRule = {\n name: \"ambiguous-step\",\n\n check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[] {\n return matchedSteps\n .filter(step => step.definitions.length > 1)\n .map(step => {\n const locations = step.definitions\n .map(d => `${d.sourceFile}:${d.line}`)\n .join(\", \");\n return {\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\" as const,\n message: `Step \"${step.text}\" matches multiple step definitions: ${locations}`,\n rule: \"ambiguous-step\",\n source: \"step-forge\" as const,\n };\n });\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const dependencyRule: AnalysisRule = {\n name: \"dependency-check\",\n\n check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const produced = {\n given: new Set<string>(),\n when: new Set<string>(),\n then: new Set<string>(),\n };\n\n for (const step of matchedSteps) {\n if (step.definitions.length !== 1) continue;\n\n const { dependencies, produces, stepType } = step.definitions[0];\n\n // Collect all missing required dependencies for this step\n const missing: Record<string, string[]> = {};\n for (const phase of [\"given\", \"when\", \"then\"] as const) {\n for (const [key, requirement] of Object.entries(dependencies[phase])) {\n if (requirement === \"required\" && !produced[phase].has(key)) {\n if (!missing[phase]) {\n missing[phase] = [];\n }\n missing[phase].push(key);\n }\n }\n }\n\n if (Object.keys(missing).length > 0) {\n const lines = Object.entries(missing).map(\n ([phase, keys]) =>\n `${phase.charAt(0).toUpperCase() + phase.slice(1)}: ${keys.map(k => `${phase}.${k}`).join(\", \")}`\n );\n const message = \"Missing required dependencies:\\n\" + lines.join(\"\\n\");\n\n diagnostics.push({\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\",\n message,\n rule: \"dependency-check\",\n source: \"step-forge\",\n });\n }\n\n // Add produced keys for this step\n for (const key of produces) {\n produced[stepType].add(key);\n }\n }\n\n return diagnostics;\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const undefinedStepRule: AnalysisRule = {\n name: \"undefined-step\",\n\n check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[] {\n return matchedSteps\n .filter(step => step.definitions.length === 0)\n .map(step => ({\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\" as const,\n message: `Step \"${step.text}\" does not match any step definition`,\n rule: \"undefined-step\",\n source: \"step-forge\" as const,\n }));\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\nimport { ambiguousStepRule } from \"./ambiguousStepRule.js\";\nimport { dependencyRule } from \"./dependencyRule.js\";\nimport { undefinedStepRule } from \"./undefinedStepRule.js\";\n\nexport const defaultRules: AnalysisRule[] = [\n undefinedStepRule,\n ambiguousStepRule,\n dependencyRule,\n];\n\nexport function runRules(\n rules: AnalysisRule[],\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n): Diagnostic[] {\n return rules.flatMap(rule => rule.check(scenario, matchedSteps));\n}\n","import { globFiles } from \"../globFiles.js\";\nimport { extractStepDefinitions } from \"./stepExtractor.js\";\nimport { parseFeatureFiles, parseFeatureContent } from \"./gherkinParser.js\";\nimport { matchScenarioSteps, findMatchingDefinitions } from \"./stepMatcher.js\";\nimport { defaultRules, runRules } from \"./rules/index.js\";\nimport type {\n AnalyzerConfig,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n MatchedStep,\n ParsedStep,\n} from \"./types.js\";\n\nexport type {\n AnalyzerConfig,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n MatchedStep,\n ParsedStep,\n};\n\nexport {\n extractStepDefinitions,\n parseFeatureFiles,\n parseFeatureContent,\n matchScenarioSteps,\n findMatchingDefinitions,\n defaultRules,\n};\n\nexport interface AnalyzeOptions {\n rules?: AnalysisRule[];\n}\n\nexport async function analyze(\n config: AnalyzerConfig,\n options?: AnalyzeOptions\n): Promise<Diagnostic[]> {\n const rules = options?.rules ?? defaultRules;\n\n // 1. Resolve file globs to paths\n const stepFilePaths = await globFiles(config.stepFiles);\n const featureFilePaths = await globFiles(config.featureFiles);\n\n if (stepFilePaths.length === 0) {\n return [];\n }\n if (featureFilePaths.length === 0) {\n return [];\n }\n\n // 2. Extract step metadata from step files\n const stepDefinitions = extractStepDefinitions(\n stepFilePaths,\n config.tsConfigPath\n );\n\n // 3. Parse feature files\n const scenarios = parseFeatureFiles(featureFilePaths);\n\n // 4. For each scenario, match steps and run rules\n const diagnostics: Diagnostic[] = [];\n for (const scenario of scenarios) {\n const matchedSteps = matchScenarioSteps(scenario, stepDefinitions);\n const scenarioDiags = runRules(rules, scenario, matchedSteps);\n diagnostics.push(...scenarioDiags);\n }\n\n return diagnostics;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as analyze } from "./analyzer-5H699Eg4.js";
2
+ import { t as analyze } from "./analyzer-E3Kq7Eul.js";
3
3
  //#region src/analyzer/cli.ts
4
4
  function parseArgs(args) {
5
5
  const config = {
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-cli.js","names":[],"sources":["../../src/analyzer/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { analyze } from \"./index.js\";\nimport type { AnalyzerConfig, Diagnostic } from \"./types.js\";\n\nfunction parseArgs(args: string[]): AnalyzerConfig {\n const config: AnalyzerConfig = {\n stepFiles: [],\n featureFiles: [],\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n const next = args[i + 1];\n\n if ((arg === \"--steps\" || arg === \"-s\") && next) {\n config.stepFiles.push(next);\n i++;\n } else if ((arg === \"--features\" || arg === \"-f\") && next) {\n config.featureFiles.push(next);\n i++;\n } else if (arg === \"--tsconfig\" && next) {\n config.tsConfigPath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n printUsage();\n process.exit(0);\n }\n }\n\n return config;\n}\n\nfunction printUsage() {\n console.log(`Usage: step-forge-analyze [options]\n\nOptions:\n -s, --steps <glob> Glob pattern for step definition files (repeatable)\n -f, --features <glob> Glob pattern for feature files (repeatable)\n --tsconfig <path> Path to tsconfig.json (default: auto-detect)\n -h, --help Show this help message\n\nExample:\n step-forge-analyze --steps \"features/steps/**/*.ts\" --features \"features/**/*.feature\"\n`);\n}\n\nfunction formatDiagnostic(diag: Diagnostic): string {\n const location = `${diag.file}:${diag.range.startLine}:${diag.range.startColumn}`;\n return `${location} - ${diag.severity}: ${diag.message}`;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const config = parseArgs(args);\n\n if (config.stepFiles.length === 0 || config.featureFiles.length === 0) {\n console.error(\n \"Error: Both --steps and --features are required.\\n\"\n );\n printUsage();\n process.exit(1);\n }\n\n const diagnostics = await analyze(config);\n\n if (diagnostics.length === 0) {\n console.log(\"No issues found.\");\n process.exit(0);\n }\n\n const errors = diagnostics.filter((d) => d.severity === \"error\");\n const warnings = diagnostics.filter((d) => d.severity === \"warning\");\n\n for (const diag of diagnostics) {\n console.log(formatDiagnostic(diag));\n }\n\n console.log(\n `\\nFound ${errors.length} error(s) and ${warnings.length} warning(s).`\n );\n process.exit(errors.length > 0 ? 1 : 0);\n}\n\n// Only run when invoked directly (not when imported by Cucumber or other tools)\nconst isDirectRun =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"analyzer-cli.js\") ||\n process.argv[1]?.endsWith(\"analyzer-cli.ts\");\n\nif (isDirectRun) {\n main().catch((err) => {\n console.error(\"Analyzer failed:\", err);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAKA,SAAS,UAAU,MAAgC;CACjD,MAAM,SAAyB;EAC7B,WAAW,CAAC;EACZ,cAAc,CAAC;CACjB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,IAAI;EAEtB,KAAK,QAAQ,aAAa,QAAQ,SAAS,MAAM;GAC/C,OAAO,UAAU,KAAK,IAAI;GAC1B;EACF,OAAO,KAAK,QAAQ,gBAAgB,QAAQ,SAAS,MAAM;GACzD,OAAO,aAAa,KAAK,IAAI;GAC7B;EACF,OAAO,IAAI,QAAQ,gBAAgB,MAAM;GACvC,OAAO,eAAe;GACtB;EACF,OAAO,IAAI,QAAQ,YAAY,QAAQ,MAAM;GAC3C,WAAW;GACX,QAAQ,KAAK,CAAC;EAChB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aAAa;CACpB,QAAQ,IAAI;;;;;;;;;;CAUb;AACD;AAEA,SAAS,iBAAiB,MAA0B;CAElD,OAAO,GAAG,GADU,KAAK,KAAK,GAAG,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,cACjD,KAAK,KAAK,SAAS,IAAI,KAAK;AACjD;AAEA,eAAe,OAAO;CAEpB,MAAM,SAAS,UADF,QAAQ,KAAK,MAAM,CACJ,CAAC;CAE7B,IAAI,OAAO,UAAU,WAAW,KAAK,OAAO,aAAa,WAAW,GAAG;EACrE,QAAQ,MACN,oDACF;EACA,WAAW;EACX,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,cAAc,MAAM,QAAQ,MAAM;CAExC,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IAAI,kBAAkB;EAC9B,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,YAAY,QAAQ,MAAM,EAAE,aAAa,OAAO;CAC/D,MAAM,WAAW,YAAY,QAAQ,MAAM,EAAE,aAAa,SAAS;CAEnE,KAAK,MAAM,QAAQ,aACjB,QAAQ,IAAI,iBAAiB,IAAI,CAAC;CAGpC,QAAQ,IACN,WAAW,OAAO,OAAO,gBAAgB,SAAS,OAAO,aAC3D;CACA,QAAQ,KAAK,OAAO,SAAS,IAAI,IAAI,CAAC;AACxC;AAQA,IAJE,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,GAG3C,KAAK,CAAC,CAAC,OAAO,QAAQ;CACpB,QAAQ,MAAM,oBAAoB,GAAG;CACrC,QAAQ,KAAK,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"analyzer-cli.js","names":[],"sources":["../../src/analyzer/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { analyze } from \"./index.js\";\nimport type { AnalyzerConfig, Diagnostic } from \"./types.js\";\n\nfunction parseArgs(args: string[]): AnalyzerConfig {\n const config: AnalyzerConfig = {\n stepFiles: [],\n featureFiles: [],\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n const next = args[i + 1];\n\n if ((arg === \"--steps\" || arg === \"-s\") && next) {\n config.stepFiles.push(next);\n i++;\n } else if ((arg === \"--features\" || arg === \"-f\") && next) {\n config.featureFiles.push(next);\n i++;\n } else if (arg === \"--tsconfig\" && next) {\n config.tsConfigPath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n printUsage();\n process.exit(0);\n }\n }\n\n return config;\n}\n\nfunction printUsage() {\n console.log(`Usage: step-forge-analyze [options]\n\nOptions:\n -s, --steps <glob> Glob pattern for step definition files (repeatable)\n -f, --features <glob> Glob pattern for feature files (repeatable)\n --tsconfig <path> Path to tsconfig.json (default: auto-detect)\n -h, --help Show this help message\n\nExample:\n step-forge-analyze --steps \"features/steps/**/*.ts\" --features \"features/**/*.feature\"\n`);\n}\n\nfunction formatDiagnostic(diag: Diagnostic): string {\n const location = `${diag.file}:${diag.range.startLine}:${diag.range.startColumn}`;\n return `${location} - ${diag.severity}: ${diag.message}`;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const config = parseArgs(args);\n\n if (config.stepFiles.length === 0 || config.featureFiles.length === 0) {\n console.error(\"Error: Both --steps and --features are required.\\n\");\n printUsage();\n process.exit(1);\n }\n\n const diagnostics = await analyze(config);\n\n if (diagnostics.length === 0) {\n console.log(\"No issues found.\");\n process.exit(0);\n }\n\n const errors = diagnostics.filter(d => d.severity === \"error\");\n const warnings = diagnostics.filter(d => d.severity === \"warning\");\n\n for (const diag of diagnostics) {\n console.log(formatDiagnostic(diag));\n }\n\n console.log(\n `\\nFound ${errors.length} error(s) and ${warnings.length} warning(s).`\n );\n process.exit(errors.length > 0 ? 1 : 0);\n}\n\n// Only run when invoked directly (not when imported by Cucumber or other tools)\nconst isDirectRun =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"analyzer-cli.js\") ||\n process.argv[1]?.endsWith(\"analyzer-cli.ts\");\n\nif (isDirectRun) {\n main().catch(err => {\n console.error(\"Analyzer failed:\", err);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAKA,SAAS,UAAU,MAAgC;CACjD,MAAM,SAAyB;EAC7B,WAAW,CAAC;EACZ,cAAc,CAAC;CACjB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,IAAI;EAEtB,KAAK,QAAQ,aAAa,QAAQ,SAAS,MAAM;GAC/C,OAAO,UAAU,KAAK,IAAI;GAC1B;EACF,OAAO,KAAK,QAAQ,gBAAgB,QAAQ,SAAS,MAAM;GACzD,OAAO,aAAa,KAAK,IAAI;GAC7B;EACF,OAAO,IAAI,QAAQ,gBAAgB,MAAM;GACvC,OAAO,eAAe;GACtB;EACF,OAAO,IAAI,QAAQ,YAAY,QAAQ,MAAM;GAC3C,WAAW;GACX,QAAQ,KAAK,CAAC;EAChB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aAAa;CACpB,QAAQ,IAAI;;;;;;;;;;CAUb;AACD;AAEA,SAAS,iBAAiB,MAA0B;CAElD,OAAO,GAAG,GADU,KAAK,KAAK,GAAG,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,cACjD,KAAK,KAAK,SAAS,IAAI,KAAK;AACjD;AAEA,eAAe,OAAO;CAEpB,MAAM,SAAS,UADF,QAAQ,KAAK,MAAM,CACJ,CAAC;CAE7B,IAAI,OAAO,UAAU,WAAW,KAAK,OAAO,aAAa,WAAW,GAAG;EACrE,QAAQ,MAAM,oDAAoD;EAClE,WAAW;EACX,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,cAAc,MAAM,QAAQ,MAAM;CAExC,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IAAI,kBAAkB;EAC9B,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,YAAY,QAAO,MAAK,EAAE,aAAa,OAAO;CAC7D,MAAM,WAAW,YAAY,QAAO,MAAK,EAAE,aAAa,SAAS;CAEjE,KAAK,MAAM,QAAQ,aACjB,QAAQ,IAAI,iBAAiB,IAAI,CAAC;CAGpC,QAAQ,IACN,WAAW,OAAO,OAAO,gBAAgB,SAAS,OAAO,aAC3D;CACA,QAAQ,KAAK,OAAO,SAAS,IAAI,IAAI,CAAC;AACxC;AAQA,IAJE,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,GAG3C,KAAK,CAAC,CAAC,OAAM,QAAO;CAClB,QAAQ,MAAM,oBAAoB,GAAG;CACrC,QAAQ,KAAK,CAAC;AAChB,CAAC"}
package/dist/analyzer.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as parseFeatureContent, i as matchScenarioSteps, n as defaultRules, o as parseFeatureFiles, r as findMatchingDefinitions, s as extractStepDefinitions, t as analyze } from "./analyzer-5H699Eg4.js";
1
+ import { a as parseFeatureContent, i as matchScenarioSteps, n as defaultRules, o as parseFeatureFiles, r as findMatchingDefinitions, s as extractStepDefinitions, t as analyze } from "./analyzer-E3Kq7Eul.js";
2
2
  export { analyze, defaultRules, extractStepDefinitions, findMatchingDefinitions, matchScenarioSteps, parseFeatureContent, parseFeatureFiles };
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- const require_gherkinParser = require("./gherkinParser-_ZgvELdy.cjs");
2
+ const require_gherkinParser = require("./gherkinParser-CbBLr_uP.cjs");
3
3
  const require_hooks = require("./hooks-Be0cjULN.cjs");
4
4
  const require_engine = require("./engine-vqA-eL_T.cjs");
5
5
  let node_path = require("node:path");
@@ -1022,8 +1022,6 @@ function isInside(child, parent) {
1022
1022
  //#endregion
1023
1023
  //#region src/runtime/interactive.ts
1024
1024
  const useColor = !process.env.NO_COLOR && (process.stdout.isTTY ?? false) === true;
1025
- const bold = (s) => useColor ? `\x1b[1m${s}\x1b[22m` : s;
1026
- const dim = (s) => useColor ? `\x1b[2m${s}\x1b[22m` : s;
1027
1025
  const red = (s) => useColor ? `\x1b[31m${s}\x1b[39m` : s;
1028
1026
  const yellow = (s) => useColor ? `\x1b[33m${s}\x1b[39m` : s;
1029
1027
  /**
@@ -1154,7 +1152,6 @@ var InteractiveSession = class {
1154
1152
  this.prompt.complete(0);
1155
1153
  return;
1156
1154
  }
1157
- if (selected.length === 1) for (const line of await this.analyzeScenario(selected[0])) this.prompt.note(line);
1158
1155
  await this.spawnRun(runArgs(choice, this.config));
1159
1156
  }
1160
1157
  /** Every scenario across the current catalog, flattened. */
@@ -1229,28 +1226,6 @@ var InteractiveSession = class {
1229
1226
  });
1230
1227
  });
1231
1228
  }
1232
- /**
1233
- * Run the static analyzer over a single scenario and return any dependency /
1234
- * undefined / ambiguous diagnostics as note lines for the results region. The
1235
- * analyzer needs the optional `typescript` peer for AST extraction; if it's
1236
- * absent we note that and skip, never failing the run.
1237
- */
1238
- async analyzeScenario(scenario) {
1239
- let analyzer;
1240
- try {
1241
- analyzer = await Promise.resolve().then(() => require("./analyzer-DRudicCk.cjs")).then((n) => n.analyzer_exports);
1242
- } catch {
1243
- return [dim(" analysis skipped: install `typescript` to enable it")];
1244
- }
1245
- try {
1246
- const stepFiles = await require_gherkinParser.globFiles(this.config.steps, this.config.cwd);
1247
- const defs = analyzer.extractStepDefinitions(stepFiles);
1248
- const matched = analyzer.matchScenarioSteps(scenario, defs);
1249
- return formatDiagnostics(analyzer.defaultRules.flatMap((rule) => rule.check(scenario, matched)), this.config.cwd);
1250
- } catch (err) {
1251
- return [dim(` analysis unavailable: ${err instanceof Error ? err.message : err}`)];
1252
- }
1253
- }
1254
1229
  status() {
1255
1230
  if (!this.armed) return " type to filter · ↑↓ move · enter run · ctrl-c quit";
1256
1231
  if (this.dirty) return " ⏸ suspended (query edited) · enter runs the selection · esc cancel";
@@ -1361,16 +1336,6 @@ function clock() {
1361
1336
  d.getSeconds()
1362
1337
  ].map((n) => String(n).padStart(2, "0")).join(":");
1363
1338
  }
1364
- function formatDiagnostics(diagnostics, cwd) {
1365
- if (diagnostics.length === 0) return [];
1366
- const lines = [bold(" analyzer:")];
1367
- for (const d of diagnostics) {
1368
- const mark = d.severity === "error" ? red("✗") : d.severity === "warning" ? yellow("!") : dim("i");
1369
- const loc = `${node_path.relative(cwd, d.file)}:${d.range.startLine}`;
1370
- lines.push(` ${mark} ${d.message} ${dim(`(${loc})`)}`);
1371
- }
1372
- return lines;
1373
- }
1374
1339
  //#endregion
1375
1340
  //#region src/runtime/cli.ts
1376
1341
  const HELP = `step-forge — native TypeScript runner for Gherkin step definitions