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