@step-forge/step-forge 0.0.12 → 0.0.14

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 -3
  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
@@ -1,432 +0,0 @@
1
- import ts from "typescript";
2
- import { StepDefinitionMeta } from "./types.js";
3
-
4
- type StepType = "given" | "when" | "then";
5
-
6
- const BUILDER_NAMES: Record<string, StepType> = {
7
- givenBuilder: "given",
8
- whenBuilder: "when",
9
- thenBuilder: "then",
10
- };
11
-
12
- export function extractStepDefinitions(
13
- filePaths: string[],
14
- tsConfigPath?: string
15
- ): StepDefinitionMeta[] {
16
- const configPath =
17
- tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
18
- let compilerOptions: ts.CompilerOptions = {
19
- target: ts.ScriptTarget.ESNext,
20
- module: ts.ModuleKind.ESNext,
21
- moduleResolution: ts.ModuleResolutionKind.Node10,
22
- strict: true,
23
- esModuleInterop: true,
24
- };
25
-
26
- if (configPath) {
27
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
28
- if (!configFile.error) {
29
- const parsed = ts.parseJsonConfigFileContent(
30
- configFile.config,
31
- ts.sys,
32
- configPath.replace(/[/\\][^/\\]+$/, "")
33
- );
34
- compilerOptions = parsed.options;
35
- }
36
- }
37
-
38
- // Ensure noEmit so we don't write files
39
- compilerOptions.noEmit = true;
40
-
41
- const program = ts.createProgram(filePaths, compilerOptions);
42
- const checker = program.getTypeChecker();
43
- const results: StepDefinitionMeta[] = [];
44
-
45
- for (const filePath of filePaths) {
46
- const sourceFile = program.getSourceFile(filePath);
47
- if (!sourceFile) continue;
48
- const fileResults = extractFromSourceFile(sourceFile, checker);
49
- results.push(...fileResults);
50
- }
51
-
52
- return results;
53
- }
54
-
55
- function extractFromSourceFile(
56
- sourceFile: ts.SourceFile,
57
- checker: ts.TypeChecker
58
- ): StepDefinitionMeta[] {
59
- const results: StepDefinitionMeta[] = [];
60
-
61
- function visit(node: ts.Node) {
62
- // Look for .register() call expressions
63
- if (
64
- ts.isCallExpression(node) &&
65
- ts.isPropertyAccessExpression(node.expression) &&
66
- node.expression.name.text === "register"
67
- ) {
68
- const meta = extractFromRegisterCall(node, sourceFile, checker);
69
- if (meta) {
70
- results.push(meta);
71
- }
72
- }
73
- ts.forEachChild(node, visit);
74
- }
75
-
76
- visit(sourceFile);
77
- return results;
78
- }
79
-
80
- function extractFromRegisterCall(
81
- registerCall: ts.CallExpression,
82
- sourceFile: ts.SourceFile,
83
- checker: ts.TypeChecker
84
- ): StepDefinitionMeta | null {
85
- // Walk backwards through the method chain to find all parts
86
- // Pattern: builder().statement(...).dependencies?(...).step(...).register()
87
- // Or: Variable("...").dependencies?(...).step(...).register()
88
-
89
- const chain = collectCallChain(registerCall);
90
-
91
- let stepType: StepType | null = null;
92
- let expression: string | null = null;
93
- let dependencies: StepDefinitionMeta["dependencies"] = {
94
- given: {},
95
- when: {},
96
- then: {},
97
- };
98
- let produces: string[] = [];
99
-
100
- for (const link of chain) {
101
- const name = getCallName(link);
102
- if (!name) continue;
103
-
104
- if (name === "register") {
105
- // Already at register, continue
106
- continue;
107
- }
108
-
109
- if (name === "step") {
110
- produces = extractProducedKeys(link, checker);
111
- continue;
112
- }
113
-
114
- if (name === "dependencies") {
115
- dependencies = extractDependencies(link);
116
- continue;
117
- }
118
-
119
- if (name === "statement") {
120
- expression = extractExpression(link);
121
- // Try to find the builder type by continuing up the chain
122
- continue;
123
- }
124
-
125
- // Check if this is a builder call like givenBuilder()
126
- if (BUILDER_NAMES[name]) {
127
- stepType = BUILDER_NAMES[name];
128
- continue;
129
- }
130
- }
131
-
132
- // If we didn't find the builder or expression in the chain, try the "re-exported" pattern:
133
- // const Given = givenBuilder<T>().statement;
134
- // Given("foo").step(...).register()
135
- if (!stepType || !expression) {
136
- const reExport = resolveReExportedCall(chain, checker);
137
- if (reExport) {
138
- if (!stepType) stepType = reExport.stepType;
139
- if (!expression) expression = reExport.expression;
140
- }
141
- }
142
-
143
- if (!stepType || !expression) {
144
- return null;
145
- }
146
-
147
- const line =
148
- sourceFile.getLineAndCharacterOfPosition(registerCall.getStart()).line + 1;
149
-
150
- return {
151
- stepType,
152
- expression,
153
- dependencies,
154
- produces,
155
- sourceFile: sourceFile.fileName,
156
- line,
157
- };
158
- }
159
-
160
- /**
161
- * Collect all call expressions in the method chain, from register() back to the origin.
162
- */
163
- function collectCallChain(call: ts.CallExpression): ts.CallExpression[] {
164
- const chain: ts.CallExpression[] = [call];
165
- let current: ts.Expression = call.expression;
166
-
167
- while (true) {
168
- // Walk through PropertyAccessExpression to find the next call
169
- if (ts.isPropertyAccessExpression(current)) {
170
- current = current.expression;
171
- }
172
-
173
- if (ts.isCallExpression(current)) {
174
- chain.push(current);
175
- current = current.expression;
176
- } else {
177
- break;
178
- }
179
- }
180
-
181
- return chain;
182
- }
183
-
184
- function getCallName(call: ts.CallExpression): string | null {
185
- const expr = call.expression;
186
-
187
- // .method() pattern
188
- if (ts.isPropertyAccessExpression(expr)) {
189
- return expr.name.text;
190
- }
191
-
192
- // direct call: functionName()
193
- if (ts.isIdentifier(expr)) {
194
- return expr.text;
195
- }
196
-
197
- return null;
198
- }
199
-
200
- function extractExpression(statementCall: ts.CallExpression): string | null {
201
- const arg = statementCall.arguments[0];
202
- if (!arg) return null;
203
-
204
- // String literal: .statement("a user")
205
- if (ts.isStringLiteral(arg)) {
206
- return arg.text;
207
- }
208
-
209
- // Arrow function with template literal: .statement((name: string) => `a user named ${name}`)
210
- if (ts.isArrowFunction(arg)) {
211
- return extractExpressionFromArrowFunction(arg);
212
- }
213
-
214
- return null;
215
- }
216
-
217
- function extractExpressionFromArrowFunction(
218
- fn: ts.ArrowFunction
219
- ): string | null {
220
- const params = fn.parameters.map((p) => p.name.getText());
221
-
222
- // The body should be a template expression or string literal
223
- let body = fn.body;
224
-
225
- // If wrapped in a block with a return, unwrap
226
- if (ts.isBlock(body)) {
227
- const returnStmt = body.statements.find(ts.isReturnStatement);
228
- if (returnStmt?.expression) {
229
- body = returnStmt.expression;
230
- } else {
231
- return null;
232
- }
233
- }
234
-
235
- if (ts.isTemplateExpression(body)) {
236
- return reconstructExpressionFromTemplate(body, params);
237
- }
238
-
239
- if (ts.isNoSubstitutionTemplateLiteral(body)) {
240
- return body.text;
241
- }
242
-
243
- return null;
244
- }
245
-
246
- function reconstructExpressionFromTemplate(
247
- template: ts.TemplateExpression,
248
- paramNames: string[]
249
- ): string {
250
- let result = template.head.text;
251
-
252
- for (const span of template.templateSpans) {
253
- if (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) {
254
- result += "{string}";
255
- } else {
256
- // Non-parameter expression, use {string} as fallback
257
- result += "{string}";
258
- }
259
- result += span.literal.text;
260
- }
261
-
262
- return result;
263
- }
264
-
265
- function extractDependencies(
266
- depsCall: ts.CallExpression
267
- ): StepDefinitionMeta["dependencies"] {
268
- const deps: StepDefinitionMeta["dependencies"] = {
269
- given: {},
270
- when: {},
271
- then: {},
272
- };
273
-
274
- const arg = depsCall.arguments[0];
275
- if (!arg || !ts.isObjectLiteralExpression(arg)) return deps;
276
-
277
- for (const prop of arg.properties) {
278
- if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
279
- continue;
280
-
281
- const phase = prop.name.text as "given" | "when" | "then";
282
- if (!deps[phase]) continue;
283
-
284
- if (ts.isObjectLiteralExpression(prop.initializer)) {
285
- for (const innerProp of prop.initializer.properties) {
286
- if (
287
- ts.isPropertyAssignment(innerProp) &&
288
- ts.isIdentifier(innerProp.name) &&
289
- ts.isStringLiteral(innerProp.initializer)
290
- ) {
291
- const val = innerProp.initializer.text;
292
- if (val === "required" || val === "optional") {
293
- deps[phase][innerProp.name.text] = val;
294
- }
295
- }
296
- }
297
- }
298
- }
299
-
300
- return deps;
301
- }
302
-
303
- function extractProducedKeys(
304
- stepCall: ts.CallExpression,
305
- checker: ts.TypeChecker
306
- ): string[] {
307
- const callback = stepCall.arguments[0];
308
- if (!callback) return [];
309
-
310
- // Try to get the return type of the callback by analyzing its body
311
- if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) {
312
- return extractProducedKeysFromCallback(callback, checker);
313
- }
314
-
315
- return [];
316
- }
317
-
318
- function extractProducedKeysFromCallback(
319
- callback: ts.ArrowFunction | ts.FunctionExpression,
320
- checker: ts.TypeChecker
321
- ): string[] {
322
- const body = callback.body;
323
-
324
- // Concise arrow: () => ({ key: value })
325
- if (!ts.isBlock(body)) {
326
- return extractKeysFromExpression(body, checker);
327
- }
328
-
329
- // Block body: look at return statements
330
- const keys = new Set<string>();
331
- function visitReturn(node: ts.Node) {
332
- if (ts.isReturnStatement(node) && node.expression) {
333
- for (const key of extractKeysFromExpression(node.expression, checker)) {
334
- keys.add(key);
335
- }
336
- }
337
- ts.forEachChild(node, visitReturn);
338
- }
339
- visitReturn(body);
340
- return [...keys];
341
- }
342
-
343
- function extractKeysFromExpression(
344
- expr: ts.Expression,
345
- checker: ts.TypeChecker
346
- ): string[] {
347
- // Unwrap parenthesized expressions
348
- while (ts.isParenthesizedExpression(expr)) {
349
- expr = expr.expression;
350
- }
351
-
352
- // Object literal: { user: ..., token: ... }
353
- if (ts.isObjectLiteralExpression(expr)) {
354
- return expr.properties
355
- .filter(
356
- (p): p is ts.PropertyAssignment | ts.ShorthandPropertyAssignment =>
357
- ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)
358
- )
359
- .map((p) => p.name.getText())
360
- .filter(Boolean);
361
- }
362
-
363
- // Try type checker as fallback
364
- try {
365
- const type = checker.getTypeAtLocation(expr);
366
- return type
367
- .getProperties()
368
- .map((p) => p.name)
369
- .filter((n) => n !== "merge");
370
- } catch {
371
- return [];
372
- }
373
- }
374
-
375
- interface ReExportResult {
376
- stepType: StepType;
377
- expression: string | null;
378
- }
379
-
380
- function resolveReExportedCall(
381
- chain: ts.CallExpression[],
382
- checker: ts.TypeChecker
383
- ): ReExportResult | null {
384
- // Look for the pattern: Variable("...")... where Variable was assigned from builderType().statement
385
- // The last call in the chain (furthest from register) should be the variable call
386
-
387
- const lastCall = chain[chain.length - 1];
388
- if (!lastCall) return null;
389
-
390
- const expr = lastCall.expression;
391
-
392
- // If it's an identifier (like "Given", "When", "Then"), trace its declaration
393
- let identifier: ts.Identifier | null = null;
394
- if (ts.isIdentifier(expr)) {
395
- identifier = expr;
396
- } else if (
397
- ts.isPropertyAccessExpression(expr) &&
398
- ts.isIdentifier(expr.expression)
399
- ) {
400
- identifier = expr.expression;
401
- }
402
-
403
- if (!identifier) return null;
404
-
405
- const symbol = checker.getSymbolAtLocation(identifier);
406
- if (!symbol) return null;
407
-
408
- const decl = symbol.valueDeclaration;
409
- if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer)
410
- return null;
411
-
412
- // Check if initializer is builderType<T>().statement
413
- const init = decl.initializer;
414
-
415
- // Pattern: givenBuilder<T>().statement (PropertyAccessExpression)
416
- if (ts.isPropertyAccessExpression(init) && init.name.text === "statement") {
417
- const callExpr = init.expression;
418
- if (ts.isCallExpression(callExpr)) {
419
- const callee = callExpr.expression;
420
- if (ts.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {
421
- // The lastCall IS the statement call — extract expression from it
422
- const expression = extractExpression(lastCall);
423
- return {
424
- stepType: BUILDER_NAMES[callee.text],
425
- expression,
426
- };
427
- }
428
- }
429
- }
430
-
431
- return null;
432
- }
@@ -1,85 +0,0 @@
1
- import { MatchedStep, ParsedScenario, StepDefinitionMeta } from "./types.js";
2
-
3
- interface CompiledPattern {
4
- regex: RegExp;
5
- definition: StepDefinitionMeta;
6
- }
7
-
8
- function compileDefinitions(
9
- definitions: StepDefinitionMeta[]
10
- ): CompiledPattern[] {
11
- const compiled: CompiledPattern[] = [];
12
- for (const def of definitions) {
13
- try {
14
- // Replace {paramType} placeholders with (.+) to match any value,
15
- // matching the Step Forge runtime behavior where all params use {string}
16
- // and values are coerced at runtime via typeCoercer.
17
- const placeholder = "###PLACEHOLDER###";
18
- const regexStr = def.expression
19
- .replace(/\{[^}]+\}/g, placeholder)
20
- .replace(/[.*+?^$()|[\]\\]/g, "\\$&")
21
- .replace(new RegExp(placeholder, "g"), "(.+)");
22
- compiled.push({
23
- regex: new RegExp(`^${regexStr}$`, "i"),
24
- definition: def,
25
- });
26
- } catch {
27
- // Skip definitions with invalid expressions
28
- }
29
- }
30
- return compiled;
31
- }
32
-
33
- export function matchScenarioSteps(
34
- scenario: ParsedScenario,
35
- definitions: StepDefinitionMeta[]
36
- ): MatchedStep[] {
37
- const compiled = compileDefinitions(definitions);
38
-
39
- return scenario.steps.map((step) => {
40
- const matches = findMatches(step.text, step.effectiveKeyword, compiled);
41
- return {
42
- ...step,
43
- definitions: matches,
44
- };
45
- });
46
- }
47
-
48
- export function findMatchingDefinitions(
49
- text: string,
50
- effectiveKeyword: "Given" | "When" | "Then",
51
- definitions: StepDefinitionMeta[]
52
- ): StepDefinitionMeta[] {
53
- const compiled = compileDefinitions(definitions);
54
- return findMatches(text, effectiveKeyword, compiled);
55
- }
56
-
57
- function findMatches(
58
- text: string,
59
- effectiveKeyword: "Given" | "When" | "Then",
60
- compiled: CompiledPattern[]
61
- ): StepDefinitionMeta[] {
62
- const keywordToStepType: Record<string, string> = {
63
- Given: "given",
64
- When: "when",
65
- Then: "then",
66
- };
67
- const expectedStepType = keywordToStepType[effectiveKeyword];
68
-
69
- // First try to match with the correct step type
70
- const typedMatches: StepDefinitionMeta[] = [];
71
- for (const { regex, definition } of compiled) {
72
- if (definition.stepType !== expectedStepType) continue;
73
- if (regex.test(text)) typedMatches.push(definition);
74
- }
75
-
76
- if (typedMatches.length > 0) return typedMatches;
77
-
78
- // Fallback: match any step type
79
- const fallbackMatches: StepDefinitionMeta[] = [];
80
- for (const { regex, definition } of compiled) {
81
- if (regex.test(text)) fallbackMatches.push(definition);
82
- }
83
-
84
- return fallbackMatches;
85
- }
@@ -1,55 +0,0 @@
1
- export interface StepDefinitionMeta {
2
- stepType: "given" | "when" | "then";
3
- expression: string;
4
- dependencies: {
5
- given: Record<string, "required" | "optional">;
6
- when: Record<string, "required" | "optional">;
7
- then: Record<string, "required" | "optional">;
8
- };
9
- produces: string[];
10
- sourceFile: string;
11
- line: number;
12
- }
13
-
14
- export interface ParsedScenario {
15
- name: string;
16
- file: string;
17
- steps: ParsedStep[];
18
- }
19
-
20
- export interface ParsedStep {
21
- keyword: "Given" | "When" | "Then" | "And" | "But";
22
- effectiveKeyword: "Given" | "When" | "Then";
23
- text: string;
24
- line: number;
25
- column: number;
26
- }
27
-
28
- export interface MatchedStep extends ParsedStep {
29
- definitions: StepDefinitionMeta[];
30
- }
31
-
32
- export interface Diagnostic {
33
- file: string;
34
- range: {
35
- startLine: number;
36
- startColumn: number;
37
- endLine: number;
38
- endColumn: number;
39
- };
40
- severity: "error" | "warning" | "info";
41
- message: string;
42
- rule: string;
43
- source: "step-forge";
44
- }
45
-
46
- export interface AnalysisRule {
47
- name: string;
48
- check(scenario: ParsedScenario, matchedSteps: MatchedStep[]): Diagnostic[];
49
- }
50
-
51
- export interface AnalyzerConfig {
52
- stepFiles: string[];
53
- featureFiles: string[];
54
- tsConfigPath?: string;
55
- }
@@ -1,27 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export type StepType = "given" | "when" | "then";
3
-
4
- export type InferAndReplace<T, U> = never extends T ? U : T;
5
-
6
- export type HasKeys<T> =
7
- T extends Record<any, any> ? (keyof T extends never ? never : T) : T;
8
-
9
- export type EmptyObject = Record<string, never>;
10
-
11
- export type RequiredOrOptional<T> = {
12
- [K in keyof T]?: "required" | "optional";
13
- };
14
-
15
- export type GetFunctionArgs<T> = T extends (...args: infer A) => any
16
- ? A
17
- : never;
18
-
19
- export const isString = (
20
- statement: string | ((...args: [...any]) => string)
21
- ): statement is string => typeof statement === "string";
22
-
23
- export type EmptyDependencies = {
24
- given: EmptyObject;
25
- when: EmptyObject;
26
- then: EmptyObject;
27
- };