@step-forge/step-forge 0.0.11 → 0.0.13

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.
Files changed (68) hide show
  1. package/dist/analyzer-QH03uejK.js +478 -0
  2. package/dist/analyzer-QH03uejK.js.map +1 -0
  3. package/dist/analyzer-cli.d.ts +1 -0
  4. package/dist/analyzer-cli.js +69 -0
  5. package/dist/analyzer-cli.js.map +1 -0
  6. package/dist/analyzer.d.ts +73 -0
  7. package/dist/analyzer.js +2 -0
  8. package/dist/step-forge.cjs +727 -0
  9. package/dist/step-forge.cjs.map +1 -0
  10. package/dist/step-forge.d.cts +326 -0
  11. package/dist/step-forge.d.ts +326 -0
  12. package/dist/step-forge.js +695 -0
  13. package/dist/step-forge.js.map +1 -0
  14. package/package.json +3 -2
  15. package/.claude/settings.local.json +0 -18
  16. package/.eslintignore +0 -6
  17. package/.eslintrc +0 -18
  18. package/.prettierignore +0 -6
  19. package/.prettierrc +0 -15
  20. package/CLAUDE.md +0 -115
  21. package/cucumber.mjs +0 -39
  22. package/docs/assets/state_deps.gif +0 -0
  23. package/features/TESTING.md +0 -100
  24. package/features/analyzer/analyzer.feature +0 -110
  25. package/features/analyzer/fixtures/ambiguous-step.feature +0 -5
  26. package/features/analyzer/fixtures/missing-given-dep.feature +0 -5
  27. package/features/analyzer/fixtures/missing-multiple-deps.feature +0 -6
  28. package/features/analyzer/fixtures/missing-when-dep.feature +0 -5
  29. package/features/analyzer/fixtures/steps.ts +0 -113
  30. package/features/analyzer/fixtures/undefined-step.feature +0 -5
  31. package/features/analyzer/fixtures/undefined-with-missing-dep.feature +0 -5
  32. package/features/analyzer/fixtures/valid-and-but-keywords.feature +0 -8
  33. package/features/analyzer/fixtures/valid-deps.feature +0 -6
  34. package/features/analyzer/fixtures/valid-no-deps.feature +0 -6
  35. package/features/analyzer/fixtures/valid-optional-deps.feature +0 -6
  36. package/features/analyzer/fixtures/valid-variables.feature +0 -6
  37. package/features/basic.feature +0 -21
  38. package/features/exported.feature +0 -6
  39. package/features/steps/analyzerSteps.ts +0 -67
  40. package/features/steps/commonSteps.ts +0 -93
  41. package/features/steps/exportedSteps.ts +0 -42
  42. package/features/steps/world.ts +0 -29
  43. package/src/analyzer/cli.ts +0 -96
  44. package/src/analyzer/gherkinParser.ts +0 -179
  45. package/src/analyzer/index.ts +0 -89
  46. package/src/analyzer/rules/ambiguousStepRule.ts +0 -36
  47. package/src/analyzer/rules/dependencyRule.ts +0 -71
  48. package/src/analyzer/rules/index.ts +0 -23
  49. package/src/analyzer/rules/undefinedStepRule.ts +0 -31
  50. package/src/analyzer/stepExtractor.ts +0 -432
  51. package/src/analyzer/stepMatcher.ts +0 -85
  52. package/src/analyzer/types.ts +0 -55
  53. package/src/builderTypeUtils.ts +0 -27
  54. package/src/common.ts +0 -138
  55. package/src/given.ts +0 -102
  56. package/src/index.ts +0 -46
  57. package/src/then.ts +0 -128
  58. package/src/utils.ts +0 -74
  59. package/src/when.ts +0 -118
  60. package/src/world.ts +0 -90
  61. package/test/givenCompilationTests.ts +0 -130
  62. package/test/testUtils.ts +0 -30
  63. package/test/thenCompilationTests.ts +0 -207
  64. package/test/whenCompilationTests.ts +0 -157
  65. package/ts-node-esm-register.js +0 -8
  66. package/tsconfig.cucumber.json +0 -14
  67. package/tsconfig.json +0 -29
  68. package/tsdown.config.ts +0 -35
@@ -0,0 +1,727 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _cucumber_cucumber = require("@cucumber/cucumber");
25
+ let lodash = require("lodash");
26
+ lodash = __toESM(lodash, 1);
27
+ let node_fs_promises = require("node:fs/promises");
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path, 1);
30
+ let typescript = require("typescript");
31
+ typescript = __toESM(typescript, 1);
32
+ let node_fs = require("node:fs");
33
+ node_fs = __toESM(node_fs, 1);
34
+ let _cucumber_gherkin = require("@cucumber/gherkin");
35
+ let _cucumber_messages = require("@cucumber/messages");
36
+ _cucumber_messages = __toESM(_cucumber_messages, 1);
37
+ //#region src/builderTypeUtils.ts
38
+ const isString = (statement) => typeof statement === "string";
39
+ //#endregion
40
+ //#region src/utils.ts
41
+ const requireFromGiven = (keys, world) => {
42
+ keys.forEach((key) => {
43
+ if (!world.given[key]) throw new Error(`Key ${String(key)} is required in given state`);
44
+ });
45
+ return keys.reduce((acc, key) => {
46
+ return {
47
+ ...acc,
48
+ [key]: world.given[key]
49
+ };
50
+ }, {});
51
+ };
52
+ const requireFromWhen = (keys, world) => {
53
+ keys.forEach((key) => {
54
+ if (!world.when[key]) throw new Error(`Key ${String(key)} is required in when state`);
55
+ });
56
+ return keys.reduce((acc, key) => {
57
+ return {
58
+ ...acc,
59
+ [key]: world.when[key]
60
+ };
61
+ }, {});
62
+ };
63
+ const requireFromThen = (keys, world) => {
64
+ keys.forEach((key) => {
65
+ if (!world.then[key]) throw new Error(`Key ${String(key)} is required in then state`);
66
+ });
67
+ return keys.reduce((acc, key) => {
68
+ return {
69
+ ...acc,
70
+ [key]: world.then[key]
71
+ };
72
+ }, {});
73
+ };
74
+ const typeCoercer = (value) => {
75
+ const numberValue = parseInt(value, 10);
76
+ if (!isNaN(numberValue)) return numberValue;
77
+ if (value === "true") return true;
78
+ if (value === "false") return false;
79
+ return value;
80
+ };
81
+ //#endregion
82
+ //#region src/common.ts
83
+ const cucFunctionMap = {
84
+ given: _cucumber_cucumber.Given,
85
+ when: _cucumber_cucumber.When,
86
+ then: _cucumber_cucumber.Then
87
+ };
88
+ const addStep = (statement, stepType, dependencies = {
89
+ given: {},
90
+ when: {},
91
+ then: {}
92
+ }) => (stepFunction) => {
93
+ const statementFunction = statement;
94
+ const { given: givenDependencies, when: whenDependencies, then: thenDependencies } = dependencies;
95
+ return {
96
+ statement,
97
+ dependencies,
98
+ stepType,
99
+ stepFunction,
100
+ register: () => {
101
+ const argCount = statementFunction.length;
102
+ const statement = statementFunction(...Array.from({ length: argCount }, () => "{string}"));
103
+ const cucStepFunction = Object.defineProperty(async function(...args) {
104
+ const coercedArgs = args.map(typeCoercer);
105
+ const ensuredGivenValues = requireFromGiven(Object.entries(givenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
106
+ const narrowedGiven = {
107
+ ...lodash.default.pick(this.given, Object.keys(givenDependencies ?? {})),
108
+ ...ensuredGivenValues
109
+ };
110
+ const ensuredWhenValues = requireFromWhen(Object.entries(whenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
111
+ const narrowedWhen = {
112
+ ...lodash.default.pick(this.when, Object.keys(whenDependencies ?? {})),
113
+ ...ensuredWhenValues
114
+ };
115
+ const ensuredThenValues = requireFromThen(Object.entries(thenDependencies ?? {}).filter(([, value]) => value === "required").map(([key]) => key), this);
116
+ const result = await stepFunction({
117
+ variables: coercedArgs,
118
+ given: narrowedGiven,
119
+ when: narrowedWhen,
120
+ then: {
121
+ ...lodash.default.pick(this.then, Object.keys(thenDependencies ?? {})),
122
+ ...ensuredThenValues
123
+ }
124
+ });
125
+ this[stepType].merge({ ...result });
126
+ }, "length", {
127
+ value: argCount,
128
+ configurable: true
129
+ });
130
+ const cucStep = cucFunctionMap[stepType];
131
+ cucStep(statement, cucStepFunction);
132
+ return {
133
+ stepType,
134
+ dependencies,
135
+ statement: statementFunction,
136
+ stepFunction
137
+ };
138
+ }
139
+ };
140
+ };
141
+ //#endregion
142
+ //#region src/given.ts
143
+ const givenDependencies = (statement, stepType) => (dependencies) => {
144
+ return { step: addStep(statement, stepType, {
145
+ ...dependencies,
146
+ when: {},
147
+ then: {}
148
+ }) };
149
+ };
150
+ const givenStatement = (stepType) => (statement) => {
151
+ let normalizedStatement;
152
+ if (isString(statement)) normalizedStatement = (() => statement);
153
+ else normalizedStatement = statement;
154
+ return {
155
+ dependencies: givenDependencies(normalizedStatement, stepType),
156
+ step: addStep(normalizedStatement, stepType)
157
+ };
158
+ };
159
+ const givenBuilder = () => {
160
+ return { statement: givenStatement("given") };
161
+ };
162
+ //#endregion
163
+ //#region src/when.ts
164
+ const whenDependencies = (statement, stepType) => (dependencies) => {
165
+ return { step: addStep(statement, stepType, {
166
+ given: dependencies.given ?? {},
167
+ when: dependencies.when ?? {},
168
+ then: {}
169
+ }) };
170
+ };
171
+ const whenStatement = (stepType) => (statement) => {
172
+ let normalizedStatement;
173
+ if (isString(statement)) normalizedStatement = (() => statement);
174
+ else normalizedStatement = statement;
175
+ return {
176
+ dependencies: whenDependencies(normalizedStatement, stepType),
177
+ step: addStep(normalizedStatement, stepType)
178
+ };
179
+ };
180
+ const whenBuilder = () => {
181
+ return { statement: whenStatement("when") };
182
+ };
183
+ //#endregion
184
+ //#region src/then.ts
185
+ const thenDependencies = (statement, stepType) => (dependencies) => {
186
+ return { step: addStep(statement, stepType, {
187
+ given: dependencies.given ?? {},
188
+ when: dependencies.when ?? {},
189
+ then: dependencies.then ?? {}
190
+ }) };
191
+ };
192
+ const thenStatement = (stepType) => (statement) => {
193
+ let normalizedStatement;
194
+ if (isString(statement)) normalizedStatement = (() => statement);
195
+ else normalizedStatement = statement;
196
+ return {
197
+ dependencies: thenDependencies(normalizedStatement, stepType),
198
+ step: addStep(normalizedStatement, stepType)
199
+ };
200
+ };
201
+ const thenBuilder = () => {
202
+ return { statement: thenStatement("then") };
203
+ };
204
+ //#endregion
205
+ //#region src/world.ts
206
+ function mergeCustomizer(objValue, srcValue) {
207
+ if (lodash.default.isArray(objValue)) return objValue.concat(srcValue);
208
+ else if (objValue && !lodash.default.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
209
+ return objValue;
210
+ }
211
+ var BasicWorld = class {
212
+ givenState = {};
213
+ whenState = {};
214
+ thenState = {};
215
+ get given() {
216
+ return {
217
+ ...this.givenState,
218
+ merge: (newState) => {
219
+ this.givenState = lodash.default.merge({ ...this.givenState }, newState, mergeCustomizer);
220
+ }
221
+ };
222
+ }
223
+ get when() {
224
+ return {
225
+ ...this.whenState,
226
+ merge: (newState) => {
227
+ this.whenState = lodash.default.merge({ ...this.whenState }, newState, mergeCustomizer);
228
+ }
229
+ };
230
+ }
231
+ get then() {
232
+ return {
233
+ ...this.thenState,
234
+ merge: (newState) => {
235
+ this.thenState = lodash.default.merge({ ...this.thenState }, newState, mergeCustomizer);
236
+ }
237
+ };
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region src/analyzer/stepExtractor.ts
242
+ const BUILDER_NAMES = {
243
+ givenBuilder: "given",
244
+ whenBuilder: "when",
245
+ thenBuilder: "then"
246
+ };
247
+ function extractStepDefinitions(filePaths, tsConfigPath) {
248
+ const configPath = tsConfigPath ?? typescript.default.findConfigFile(process.cwd(), typescript.default.sys.fileExists);
249
+ let compilerOptions = {
250
+ target: typescript.default.ScriptTarget.ESNext,
251
+ module: typescript.default.ModuleKind.ESNext,
252
+ moduleResolution: typescript.default.ModuleResolutionKind.Node10,
253
+ strict: true,
254
+ esModuleInterop: true
255
+ };
256
+ if (configPath) {
257
+ const configFile = typescript.default.readConfigFile(configPath, typescript.default.sys.readFile);
258
+ if (!configFile.error) compilerOptions = typescript.default.parseJsonConfigFileContent(configFile.config, typescript.default.sys, configPath.replace(/[/\\][^/\\]+$/, "")).options;
259
+ }
260
+ compilerOptions.noEmit = true;
261
+ const program = typescript.default.createProgram(filePaths, compilerOptions);
262
+ const checker = program.getTypeChecker();
263
+ const results = [];
264
+ for (const filePath of filePaths) {
265
+ const sourceFile = program.getSourceFile(filePath);
266
+ if (!sourceFile) continue;
267
+ const fileResults = extractFromSourceFile(sourceFile, checker);
268
+ results.push(...fileResults);
269
+ }
270
+ return results;
271
+ }
272
+ function extractFromSourceFile(sourceFile, checker) {
273
+ const results = [];
274
+ function visit(node) {
275
+ if (typescript.default.isCallExpression(node) && typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "register") {
276
+ const meta = extractFromRegisterCall(node, sourceFile, checker);
277
+ if (meta) results.push(meta);
278
+ }
279
+ typescript.default.forEachChild(node, visit);
280
+ }
281
+ visit(sourceFile);
282
+ return results;
283
+ }
284
+ function extractFromRegisterCall(registerCall, sourceFile, checker) {
285
+ const chain = collectCallChain(registerCall);
286
+ let stepType = null;
287
+ let expression = null;
288
+ let dependencies = {
289
+ given: {},
290
+ when: {},
291
+ then: {}
292
+ };
293
+ let produces = [];
294
+ for (const link of chain) {
295
+ const name = getCallName(link);
296
+ if (!name) continue;
297
+ if (name === "register") continue;
298
+ if (name === "step") {
299
+ produces = extractProducedKeys(link, checker);
300
+ continue;
301
+ }
302
+ if (name === "dependencies") {
303
+ dependencies = extractDependencies(link);
304
+ continue;
305
+ }
306
+ if (name === "statement") {
307
+ expression = extractExpression(link);
308
+ continue;
309
+ }
310
+ if (BUILDER_NAMES[name]) {
311
+ stepType = BUILDER_NAMES[name];
312
+ continue;
313
+ }
314
+ }
315
+ if (!stepType || !expression) {
316
+ const reExport = resolveReExportedCall(chain, checker);
317
+ if (reExport) {
318
+ if (!stepType) stepType = reExport.stepType;
319
+ if (!expression) expression = reExport.expression;
320
+ }
321
+ }
322
+ if (!stepType || !expression) return null;
323
+ const line = sourceFile.getLineAndCharacterOfPosition(registerCall.getStart()).line + 1;
324
+ return {
325
+ stepType,
326
+ expression,
327
+ dependencies,
328
+ produces,
329
+ sourceFile: sourceFile.fileName,
330
+ line
331
+ };
332
+ }
333
+ /**
334
+ * Collect all call expressions in the method chain, from register() back to the origin.
335
+ */
336
+ function collectCallChain(call) {
337
+ const chain = [call];
338
+ let current = call.expression;
339
+ while (true) {
340
+ if (typescript.default.isPropertyAccessExpression(current)) current = current.expression;
341
+ if (typescript.default.isCallExpression(current)) {
342
+ chain.push(current);
343
+ current = current.expression;
344
+ } else break;
345
+ }
346
+ return chain;
347
+ }
348
+ function getCallName(call) {
349
+ const expr = call.expression;
350
+ if (typescript.default.isPropertyAccessExpression(expr)) return expr.name.text;
351
+ if (typescript.default.isIdentifier(expr)) return expr.text;
352
+ return null;
353
+ }
354
+ function extractExpression(statementCall) {
355
+ const arg = statementCall.arguments[0];
356
+ if (!arg) return null;
357
+ if (typescript.default.isStringLiteral(arg)) return arg.text;
358
+ if (typescript.default.isArrowFunction(arg)) return extractExpressionFromArrowFunction(arg);
359
+ return null;
360
+ }
361
+ function extractExpressionFromArrowFunction(fn) {
362
+ const params = fn.parameters.map((p) => p.name.getText());
363
+ let body = fn.body;
364
+ if (typescript.default.isBlock(body)) {
365
+ const returnStmt = body.statements.find(typescript.default.isReturnStatement);
366
+ if (returnStmt?.expression) body = returnStmt.expression;
367
+ else return null;
368
+ }
369
+ if (typescript.default.isTemplateExpression(body)) return reconstructExpressionFromTemplate(body, params);
370
+ if (typescript.default.isNoSubstitutionTemplateLiteral(body)) return body.text;
371
+ return null;
372
+ }
373
+ function reconstructExpressionFromTemplate(template, paramNames) {
374
+ let result = template.head.text;
375
+ for (const span of template.templateSpans) {
376
+ if (typescript.default.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) result += "{string}";
377
+ else result += "{string}";
378
+ result += span.literal.text;
379
+ }
380
+ return result;
381
+ }
382
+ function extractDependencies(depsCall) {
383
+ const deps = {
384
+ given: {},
385
+ when: {},
386
+ then: {}
387
+ };
388
+ const arg = depsCall.arguments[0];
389
+ if (!arg || !typescript.default.isObjectLiteralExpression(arg)) return deps;
390
+ for (const prop of arg.properties) {
391
+ if (!typescript.default.isPropertyAssignment(prop) || !typescript.default.isIdentifier(prop.name)) continue;
392
+ const phase = prop.name.text;
393
+ if (!deps[phase]) continue;
394
+ if (typescript.default.isObjectLiteralExpression(prop.initializer)) {
395
+ for (const innerProp of prop.initializer.properties) if (typescript.default.isPropertyAssignment(innerProp) && typescript.default.isIdentifier(innerProp.name) && typescript.default.isStringLiteral(innerProp.initializer)) {
396
+ const val = innerProp.initializer.text;
397
+ if (val === "required" || val === "optional") deps[phase][innerProp.name.text] = val;
398
+ }
399
+ }
400
+ }
401
+ return deps;
402
+ }
403
+ function extractProducedKeys(stepCall, checker) {
404
+ const callback = stepCall.arguments[0];
405
+ if (!callback) return [];
406
+ if (typescript.default.isArrowFunction(callback) || typescript.default.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, checker);
407
+ return [];
408
+ }
409
+ function extractProducedKeysFromCallback(callback, checker) {
410
+ const body = callback.body;
411
+ if (!typescript.default.isBlock(body)) return extractKeysFromExpression(body, checker);
412
+ const keys = /* @__PURE__ */ new Set();
413
+ function visitReturn(node) {
414
+ if (typescript.default.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, checker)) keys.add(key);
415
+ typescript.default.forEachChild(node, visitReturn);
416
+ }
417
+ visitReturn(body);
418
+ return [...keys];
419
+ }
420
+ function extractKeysFromExpression(expr, checker) {
421
+ while (typescript.default.isParenthesizedExpression(expr)) expr = expr.expression;
422
+ if (typescript.default.isObjectLiteralExpression(expr)) return expr.properties.filter((p) => typescript.default.isPropertyAssignment(p) || typescript.default.isShorthandPropertyAssignment(p)).map((p) => p.name.getText()).filter(Boolean);
423
+ try {
424
+ return checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
425
+ } catch {
426
+ return [];
427
+ }
428
+ }
429
+ function resolveReExportedCall(chain, checker) {
430
+ const lastCall = chain[chain.length - 1];
431
+ if (!lastCall) return null;
432
+ const expr = lastCall.expression;
433
+ let identifier = null;
434
+ if (typescript.default.isIdentifier(expr)) identifier = expr;
435
+ else if (typescript.default.isPropertyAccessExpression(expr) && typescript.default.isIdentifier(expr.expression)) identifier = expr.expression;
436
+ if (!identifier) return null;
437
+ const symbol = checker.getSymbolAtLocation(identifier);
438
+ if (!symbol) return null;
439
+ const decl = symbol.valueDeclaration;
440
+ if (!decl || !typescript.default.isVariableDeclaration(decl) || !decl.initializer) return null;
441
+ const init = decl.initializer;
442
+ if (typescript.default.isPropertyAccessExpression(init) && init.name.text === "statement") {
443
+ const callExpr = init.expression;
444
+ if (typescript.default.isCallExpression(callExpr)) {
445
+ const callee = callExpr.expression;
446
+ if (typescript.default.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {
447
+ const expression = extractExpression(lastCall);
448
+ return {
449
+ stepType: BUILDER_NAMES[callee.text],
450
+ expression
451
+ };
452
+ }
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ //#endregion
458
+ //#region src/analyzer/gherkinParser.ts
459
+ function parseFeatureFiles(filePaths) {
460
+ const scenarios = [];
461
+ for (const filePath of filePaths) {
462
+ const parsed = parseFeatureContent(node_fs.readFileSync(filePath, "utf-8"), filePath);
463
+ scenarios.push(...parsed);
464
+ }
465
+ return scenarios;
466
+ }
467
+ function parseFeatureContent(content, filePath) {
468
+ const feature = new _cucumber_gherkin.Parser(new _cucumber_gherkin.AstBuilder(_cucumber_messages.IdGenerator.uuid()), new _cucumber_gherkin.GherkinClassicTokenMatcher()).parse(content).feature;
469
+ if (!feature) return [];
470
+ const featureBackground = [];
471
+ const scenarios = [];
472
+ for (const child of feature.children) {
473
+ if (child.background) featureBackground.push(...child.background.steps);
474
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath));
475
+ if (child.rule) {
476
+ const ruleBackground = [...featureBackground];
477
+ for (const ruleChild of child.rule.children) {
478
+ if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
479
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath));
480
+ }
481
+ }
482
+ }
483
+ return scenarios;
484
+ }
485
+ function expandScenario(scenario, backgroundSteps, filePath) {
486
+ if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
487
+ const bgParsed = convertSteps(backgroundSteps);
488
+ const scenarioParsed = convertSteps(scenario.steps);
489
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
490
+ return [{
491
+ name: scenario.name,
492
+ file: filePath,
493
+ steps: allSteps
494
+ }];
495
+ }
496
+ const results = [];
497
+ for (const example of scenario.examples) {
498
+ if (!example.tableHeader || example.tableBody.length === 0) continue;
499
+ const headers = example.tableHeader.cells.map((c) => c.value);
500
+ for (const row of example.tableBody) {
501
+ const values = row.cells.map((c) => c.value);
502
+ const substitution = {};
503
+ headers.forEach((h, i) => {
504
+ substitution[h] = values[i];
505
+ });
506
+ const bgParsed = convertSteps(backgroundSteps);
507
+ const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
508
+ ...step,
509
+ text: substituteExampleValues(step.text, substitution)
510
+ }));
511
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
512
+ results.push({
513
+ name: `${scenario.name} (${values.join(", ")})`,
514
+ file: filePath,
515
+ steps: allSteps
516
+ });
517
+ }
518
+ }
519
+ return results;
520
+ }
521
+ function convertSteps(steps) {
522
+ return steps.map((step) => ({
523
+ keyword: normalizeKeyword(step.keyword),
524
+ text: step.text,
525
+ line: step.location.line,
526
+ column: (step.location.column ?? 1) + step.keyword.length
527
+ }));
528
+ }
529
+ function normalizeKeyword(keyword) {
530
+ const trimmed = keyword.trim();
531
+ if (trimmed === "Given") return "Given";
532
+ if (trimmed === "When") return "When";
533
+ if (trimmed === "Then") return "Then";
534
+ if (trimmed === "And") return "And";
535
+ if (trimmed === "But") return "But";
536
+ return "Given";
537
+ }
538
+ function resolveEffectiveKeywords(steps) {
539
+ let lastEffective = "Given";
540
+ return steps.map((step) => {
541
+ let effectiveKeyword;
542
+ if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
543
+ else effectiveKeyword = step.keyword;
544
+ lastEffective = effectiveKeyword;
545
+ return {
546
+ ...step,
547
+ effectiveKeyword
548
+ };
549
+ });
550
+ }
551
+ function substituteExampleValues(text, substitution) {
552
+ let result = text;
553
+ for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
554
+ return result;
555
+ }
556
+ //#endregion
557
+ //#region src/analyzer/stepMatcher.ts
558
+ function compileDefinitions(definitions) {
559
+ const compiled = [];
560
+ for (const def of definitions) try {
561
+ const placeholder = "###PLACEHOLDER###";
562
+ const regexStr = def.expression.replace(/\{[^}]+\}/g, placeholder).replace(/[.*+?^$()|[\]\\]/g, "\\$&").replace(new RegExp(placeholder, "g"), "(.+)");
563
+ compiled.push({
564
+ regex: new RegExp(`^${regexStr}$`, "i"),
565
+ definition: def
566
+ });
567
+ } catch {}
568
+ return compiled;
569
+ }
570
+ function matchScenarioSteps(scenario, definitions) {
571
+ const compiled = compileDefinitions(definitions);
572
+ return scenario.steps.map((step) => {
573
+ const matches = findMatches(step.text, step.effectiveKeyword, compiled);
574
+ return {
575
+ ...step,
576
+ definitions: matches
577
+ };
578
+ });
579
+ }
580
+ function findMatchingDefinitions(text, effectiveKeyword, definitions) {
581
+ return findMatches(text, effectiveKeyword, compileDefinitions(definitions));
582
+ }
583
+ function findMatches(text, effectiveKeyword, compiled) {
584
+ const expectedStepType = {
585
+ Given: "given",
586
+ When: "when",
587
+ Then: "then"
588
+ }[effectiveKeyword];
589
+ const typedMatches = [];
590
+ for (const { regex, definition } of compiled) {
591
+ if (definition.stepType !== expectedStepType) continue;
592
+ if (regex.test(text)) typedMatches.push(definition);
593
+ }
594
+ if (typedMatches.length > 0) return typedMatches;
595
+ const fallbackMatches = [];
596
+ for (const { regex, definition } of compiled) if (regex.test(text)) fallbackMatches.push(definition);
597
+ return fallbackMatches;
598
+ }
599
+ //#endregion
600
+ //#region src/analyzer/rules/index.ts
601
+ const defaultRules = [
602
+ {
603
+ name: "undefined-step",
604
+ check(scenario, matchedSteps) {
605
+ return matchedSteps.filter((step) => step.definitions.length === 0).map((step) => ({
606
+ file: scenario.file,
607
+ range: {
608
+ startLine: step.line,
609
+ startColumn: step.column,
610
+ endLine: step.line,
611
+ endColumn: step.column + step.text.length
612
+ },
613
+ severity: "error",
614
+ message: `Step "${step.text}" does not match any step definition`,
615
+ rule: "undefined-step",
616
+ source: "step-forge"
617
+ }));
618
+ }
619
+ },
620
+ {
621
+ name: "ambiguous-step",
622
+ check(scenario, matchedSteps) {
623
+ return matchedSteps.filter((step) => step.definitions.length > 1).map((step) => {
624
+ const locations = step.definitions.map((d) => `${d.sourceFile}:${d.line}`).join(", ");
625
+ return {
626
+ file: scenario.file,
627
+ range: {
628
+ startLine: step.line,
629
+ startColumn: step.column,
630
+ endLine: step.line,
631
+ endColumn: step.column + step.text.length
632
+ },
633
+ severity: "error",
634
+ message: `Step "${step.text}" matches multiple step definitions: ${locations}`,
635
+ rule: "ambiguous-step",
636
+ source: "step-forge"
637
+ };
638
+ });
639
+ }
640
+ },
641
+ {
642
+ name: "dependency-check",
643
+ check(scenario, matchedSteps) {
644
+ const diagnostics = [];
645
+ const produced = {
646
+ given: /* @__PURE__ */ new Set(),
647
+ when: /* @__PURE__ */ new Set(),
648
+ then: /* @__PURE__ */ new Set()
649
+ };
650
+ for (const step of matchedSteps) {
651
+ if (step.definitions.length !== 1) continue;
652
+ const { dependencies, produces, stepType } = step.definitions[0];
653
+ const missing = {};
654
+ for (const phase of [
655
+ "given",
656
+ "when",
657
+ "then"
658
+ ]) for (const [key, requirement] of Object.entries(dependencies[phase])) if (requirement === "required" && !produced[phase].has(key)) {
659
+ if (!missing[phase]) missing[phase] = [];
660
+ missing[phase].push(key);
661
+ }
662
+ if (Object.keys(missing).length > 0) {
663
+ const message = "Missing required dependencies:\n" + Object.entries(missing).map(([phase, keys]) => `${phase.charAt(0).toUpperCase() + phase.slice(1)}: ${keys.map((k) => `${phase}.${k}`).join(", ")}`).join("\n");
664
+ diagnostics.push({
665
+ file: scenario.file,
666
+ range: {
667
+ startLine: step.line,
668
+ startColumn: step.column,
669
+ endLine: step.line,
670
+ endColumn: step.column + step.text.length
671
+ },
672
+ severity: "error",
673
+ message,
674
+ rule: "dependency-check",
675
+ source: "step-forge"
676
+ });
677
+ }
678
+ for (const key of produces) produced[stepType].add(key);
679
+ }
680
+ return diagnostics;
681
+ }
682
+ }
683
+ ];
684
+ function runRules(rules, scenario, matchedSteps) {
685
+ return rules.flatMap((rule) => rule.check(scenario, matchedSteps));
686
+ }
687
+ //#endregion
688
+ //#region src/analyzer/index.ts
689
+ async function analyze(config, options) {
690
+ const rules = options?.rules ?? defaultRules;
691
+ const stepFilePaths = await resolveGlobs(config.stepFiles);
692
+ const featureFilePaths = await resolveGlobs(config.featureFiles);
693
+ if (stepFilePaths.length === 0) return [];
694
+ if (featureFilePaths.length === 0) return [];
695
+ const stepDefinitions = extractStepDefinitions(stepFilePaths, config.tsConfigPath);
696
+ const scenarios = parseFeatureFiles(featureFilePaths);
697
+ const diagnostics = [];
698
+ for (const scenario of scenarios) {
699
+ const scenarioDiags = runRules(rules, scenario, matchScenarioSteps(scenario, stepDefinitions));
700
+ diagnostics.push(...scenarioDiags);
701
+ }
702
+ return diagnostics;
703
+ }
704
+ async function resolveGlobs(patterns) {
705
+ const files = [];
706
+ for (const pattern of patterns) for await (const file of (0, node_fs_promises.glob)(pattern)) files.push(node_path.resolve(file));
707
+ return [...new Set(files)];
708
+ }
709
+ //#endregion
710
+ //#region src/index.ts
711
+ const analyzer = {
712
+ analyze,
713
+ extractStepDefinitions,
714
+ parseFeatureFiles,
715
+ parseFeatureContent,
716
+ matchScenarioSteps,
717
+ findMatchingDefinitions,
718
+ defaultRules
719
+ };
720
+ //#endregion
721
+ exports.BasicWorld = BasicWorld;
722
+ exports.analyzer = analyzer;
723
+ exports.givenBuilder = givenBuilder;
724
+ exports.thenBuilder = thenBuilder;
725
+ exports.whenBuilder = whenBuilder;
726
+
727
+ //# sourceMappingURL=step-forge.cjs.map