@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 @@
1
+ {"version":3,"file":"step-forge.js","names":["CucGiven","CucWhen","CucThen"],"sources":["../../src/builderTypeUtils.ts","../../src/utils.ts","../../src/common.ts","../../src/given.ts","../../src/when.ts","../../src/then.ts","../../src/world.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","../../src/index.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nexport type StepType = \"given\" | \"when\" | \"then\";\n\nexport type InferAndReplace<T, U> = never extends T ? U : T;\n\nexport type HasKeys<T> =\n T extends Record<any, any> ? (keyof T extends never ? never : T) : T;\n\nexport type EmptyObject = Record<string, never>;\n\nexport type RequiredOrOptional<T> = {\n [K in keyof T]?: \"required\" | \"optional\";\n};\n\nexport type GetFunctionArgs<T> = T extends (...args: infer A) => any\n ? A\n : never;\n\nexport const isString = (\n statement: string | ((...args: [...any]) => string)\n): statement is string => typeof statement === \"string\";\n\nexport type EmptyDependencies = {\n given: EmptyObject;\n when: EmptyObject;\n then: EmptyObject;\n};\n","import { MergeableWorld } from \"./world\";\n\nexport const requireFromGiven = <G>(\n keys: (keyof G)[],\n world: MergeableWorld<G, unknown, unknown>\n) => {\n keys.forEach(key => {\n if (!world.given[key]) {\n throw new Error(`Key ${String(key)} is required in given state`);\n }\n });\n return keys.reduce(\n (acc, key) => {\n return {\n ...acc,\n [key]: world.given[key],\n };\n },\n {} as { [key in keyof G]: G[key] }\n );\n};\nexport const requireFromWhen = <W>(\n keys: (keyof W)[],\n world: MergeableWorld<unknown, W, unknown>\n) => {\n keys.forEach(key => {\n if (!world.when[key]) {\n throw new Error(`Key ${String(key)} is required in when state`);\n }\n });\n return keys.reduce(\n (acc, key) => {\n return {\n ...acc,\n [key]: world.when[key],\n };\n },\n {} as { [key in keyof W]: W[key] }\n );\n};\nexport const requireFromThen = <T>(\n keys: (keyof T)[],\n world: MergeableWorld<unknown, unknown, T>\n) => {\n keys.forEach(key => {\n if (!world.then[key]) {\n throw new Error(`Key ${String(key)} is required in then state`);\n }\n });\n return keys.reduce(\n (acc, key) => {\n return {\n ...acc,\n [key]: world.then[key],\n };\n },\n {} as { [key in keyof T]: T[key] }\n );\n};\n\nexport const typeCoercer = (value: string) => {\n // Try to parse as integer first\n const numberValue = parseInt(value, 10);\n if (!isNaN(numberValue)) {\n return numberValue;\n }\n\n // Check for boolean values\n if (value === \"true\") return true;\n if (value === \"false\") return false;\n\n // Return original string if no other type matches\n return value;\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n Given as CucGiven,\n Then as CucThen,\n When as CucWhen,\n} from \"@cucumber/cucumber\";\nimport _ from \"lodash\";\n\nimport { StepType } from \"./builderTypeUtils\";\nimport {\n requireFromGiven,\n requireFromThen,\n requireFromWhen,\n typeCoercer,\n} from \"./utils\";\nimport { MergeableWorld } from \"./world\";\n\nconst cucFunctionMap = {\n given: CucGiven,\n when: CucWhen,\n then: CucThen,\n};\n\nexport const addStep =\n <\n ResolvedStepType extends StepType,\n Statement extends (...args: any[]) => string,\n Dependencies extends {\n given: any;\n when: any;\n then: any;\n },\n Variables,\n GivenState,\n WhenState,\n ThenState,\n RestrictedGivenState,\n RestrictedWhenState,\n RestrictedThenState,\n >(\n statement: Statement,\n stepType: ResolvedStepType,\n dependencies: Dependencies = {\n given: {},\n when: {},\n then: {},\n } as Dependencies\n ) =>\n (\n stepFunction: (input: {\n variables: Variables;\n given: RestrictedGivenState;\n when: RestrictedWhenState;\n then: RestrictedThenState;\n }) => ResolvedStepType extends \"given\"\n ? Partial<GivenState>\n : ResolvedStepType extends \"when\"\n ? Partial<WhenState>\n : Partial<ThenState> | void\n ) => {\n const statementFunction = statement;\n const {\n given: givenDependencies,\n when: whenDependencies,\n then: thenDependencies,\n } = dependencies;\n return {\n statement,\n dependencies,\n stepType,\n stepFunction,\n register: () => {\n const argCount = statementFunction.length;\n const argMatchers = Array.from({ length: argCount }, () => \"{string}\");\n const statement = statementFunction(...argMatchers);\n const cucStepFunction = Object.defineProperty(\n async function (\n this: MergeableWorld<GivenState, WhenState, ThenState>,\n ...args: string[]\n ) {\n const coercedArgs = args.map(typeCoercer);\n const requiredGivenKeys = Object.entries(givenDependencies ?? {})\n .filter(([, value]) => value === \"required\")\n .map(([key]) => key);\n const ensuredGivenValues = requireFromGiven(\n requiredGivenKeys as (keyof GivenState)[],\n this\n );\n const narrowedGiven = {\n ..._.pick(this.given, Object.keys(givenDependencies ?? {})),\n ...ensuredGivenValues,\n };\n const requiredWhenKeys = Object.entries(whenDependencies ?? {})\n .filter(([, value]) => value === \"required\")\n .map(([key]) => key);\n const ensuredWhenValues = requireFromWhen(\n requiredWhenKeys as (keyof WhenState)[],\n this\n );\n const narrowedWhen = {\n ..._.pick(this.when, Object.keys(whenDependencies ?? {})),\n ...ensuredWhenValues,\n };\n const requiredThenKeys = Object.entries(thenDependencies ?? {})\n .filter(([, value]) => value === \"required\")\n .map(([key]) => key);\n const ensuredThenValues = requireFromThen(\n requiredThenKeys as (keyof ThenState)[],\n this\n );\n const narrowedThen = {\n ..._.pick(this.then, Object.keys(thenDependencies ?? {})),\n ...ensuredThenValues,\n };\n const result = await stepFunction({\n variables: coercedArgs as Variables,\n given: narrowedGiven as RestrictedGivenState,\n when: narrowedWhen as RestrictedWhenState,\n then: narrowedThen as RestrictedThenState,\n });\n this[stepType].merge({\n ...(result as any),\n });\n },\n \"length\",\n { value: argCount, configurable: true }\n );\n const cucStep = cucFunctionMap[stepType];\n cucStep(statement, cucStepFunction);\n return {\n stepType,\n dependencies,\n statement: statementFunction,\n stepFunction,\n };\n },\n };\n };\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport {\n EmptyDependencies,\n EmptyObject,\n GetFunctionArgs,\n isString,\n RequiredOrOptional,\n StepType,\n} from \"./builderTypeUtils\";\nimport { addStep } from \"./common\";\n\nconst givenDependencies =\n <\n Statement extends (...args: any[]) => string,\n ResolvedStepType extends StepType,\n Variables,\n GivenState,\n >(\n statement: Statement,\n stepType: ResolvedStepType\n ) =>\n <GivenDeps extends RequiredOrOptional<GivenState>>(dependencies: {\n given: GivenDeps;\n }) => {\n type RestrictedGivenState = {\n [K in keyof GivenState as K extends keyof GivenDeps\n ? K\n : never]: GivenDeps[K] extends \"optional\"\n ? GivenState[K] | undefined\n : GivenState[K];\n };\n type Dependencies = typeof dependencies & {\n when: EmptyObject;\n then: EmptyObject;\n };\n const fullDependencies = {\n ...dependencies,\n when: {},\n then: {},\n };\n return {\n step: addStep<\n ResolvedStepType,\n Statement,\n Dependencies,\n Variables,\n GivenState,\n never, // when state\n never, // then state\n RestrictedGivenState,\n never, // restricted when state\n never // restricted then state\n >(statement, stepType, fullDependencies),\n };\n };\n\nconst givenStatement =\n <ResolvedStepType extends StepType, GivenState>(stepType: ResolvedStepType) =>\n <Statement extends ((...args: [...any]) => string) | string>(\n statement: Statement\n ) => {\n let normalizedStatement: Statement extends string\n ? () => string\n : Statement;\n if (isString(statement)) {\n normalizedStatement = (() => statement) as any;\n } else {\n normalizedStatement = statement as any;\n }\n type NormalizedStatement = typeof normalizedStatement;\n\n type Variables = Statement extends string ? [] : GetFunctionArgs<Statement>;\n const dependencyFunc = givenDependencies<\n NormalizedStatement,\n ResolvedStepType,\n Variables,\n GivenState\n >(normalizedStatement, stepType);\n const stepFunc = addStep<\n ResolvedStepType,\n NormalizedStatement,\n EmptyDependencies,\n Variables,\n GivenState,\n never,\n never,\n never,\n never,\n never\n >(normalizedStatement, stepType);\n return {\n dependencies: dependencyFunc,\n step: stepFunc,\n };\n };\n\nexport const givenBuilder = <GivenState>() => {\n return {\n statement: givenStatement<\"given\", GivenState>(\"given\"),\n };\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport {\n EmptyDependencies,\n EmptyObject,\n GetFunctionArgs,\n isString,\n RequiredOrOptional,\n StepType,\n} from \"./builderTypeUtils\";\nimport { addStep } from \"./common\";\n\nconst whenDependencies =\n <\n Statement extends (...args: any[]) => string,\n ResolvedStepType extends StepType,\n Variables,\n GivenState,\n WhenState,\n >(\n statement: Statement,\n stepType: ResolvedStepType\n ) =>\n <\n GivenDeps extends RequiredOrOptional<GivenState>,\n WhenDeps extends RequiredOrOptional<WhenState>,\n >(dependencies: {\n given?: GivenDeps;\n when?: WhenDeps;\n }) => {\n type RestrictedGivenState = {\n [K in keyof GivenState as K extends keyof GivenDeps\n ? K\n : never]: GivenDeps[K] extends \"optional\"\n ? GivenState[K] | undefined\n : GivenState[K];\n };\n type RestrictedWhenState = {\n [K in keyof WhenState as K extends keyof WhenDeps\n ? K\n : never]: WhenDeps[K] extends \"optional\"\n ? WhenState[K] | undefined\n : WhenState[K];\n };\n type Dependencies = {\n given: GivenDeps;\n when: WhenDeps;\n then: EmptyObject;\n };\n const fullDependencies: Dependencies = {\n given: dependencies.given ?? ({} as GivenDeps),\n when: dependencies.when ?? ({} as WhenDeps),\n then: {},\n };\n return {\n step: addStep<\n ResolvedStepType,\n Statement,\n Dependencies,\n Variables,\n GivenState,\n WhenState,\n never,\n RestrictedGivenState,\n RestrictedWhenState,\n never\n >(statement, stepType, fullDependencies),\n };\n };\n\nconst whenStatement =\n <ResolvedStepType extends StepType, GivenState, WhenState>(\n stepType: ResolvedStepType\n ) =>\n <Statement extends ((...args: [...any]) => string) | string>(\n statement: Statement\n ) => {\n let normalizedStatement: Statement extends string\n ? () => string\n : Statement;\n if (isString(statement)) {\n normalizedStatement = (() => statement) as any;\n } else {\n normalizedStatement = statement as any;\n }\n type NormalizedStatement = typeof normalizedStatement;\n\n type Variables = Statement extends string ? [] : GetFunctionArgs<Statement>;\n const dependencyFunc = whenDependencies<\n NormalizedStatement,\n ResolvedStepType,\n Variables,\n GivenState,\n WhenState\n >(normalizedStatement, stepType);\n const stepFunc = addStep<\n ResolvedStepType,\n NormalizedStatement,\n EmptyDependencies,\n Variables,\n GivenState,\n WhenState,\n never,\n never,\n never,\n never\n >(normalizedStatement, stepType);\n return {\n dependencies: dependencyFunc,\n step: stepFunc,\n };\n };\n\nexport const whenBuilder = <GivenState, WhenState>() => {\n return {\n statement: whenStatement<\"when\", GivenState, WhenState>(\"when\"),\n };\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport {\n EmptyDependencies,\n GetFunctionArgs,\n isString,\n RequiredOrOptional,\n StepType,\n} from \"./builderTypeUtils\";\nimport { addStep } from \"./common\";\n\nconst thenDependencies =\n <\n Statement extends (...args: any[]) => string,\n ResolvedStepType extends StepType,\n Variables,\n GivenState,\n WhenState,\n ThenState,\n >(\n statement: Statement,\n stepType: ResolvedStepType\n ) =>\n <\n GivenDeps extends RequiredOrOptional<GivenState>,\n WhenDeps extends RequiredOrOptional<WhenState>,\n ThenDeps extends RequiredOrOptional<ThenState>,\n >(dependencies: {\n given?: GivenDeps;\n when?: WhenDeps;\n then?: ThenDeps;\n }) => {\n type RestrictedGivenState = {\n [K in keyof GivenState as K extends keyof GivenDeps\n ? K\n : never]: GivenDeps[K] extends \"optional\"\n ? GivenState[K] | undefined\n : GivenState[K];\n };\n type RestrictedWhenState = {\n [K in keyof WhenState as K extends keyof WhenDeps\n ? K\n : never]: WhenDeps[K] extends \"optional\"\n ? WhenState[K] | undefined\n : WhenState[K];\n };\n type RestrictedThenState = {\n [K in keyof ThenState as K extends keyof ThenDeps\n ? K\n : never]: ThenDeps[K] extends \"optional\"\n ? ThenState[K] | undefined\n : ThenState[K];\n };\n type Dependencies = {\n given: GivenDeps;\n when: WhenDeps;\n then: ThenDeps;\n };\n const fullDependencies: Dependencies = {\n given: dependencies.given ?? ({} as GivenDeps),\n when: dependencies.when ?? ({} as WhenDeps),\n then: dependencies.then ?? ({} as ThenDeps),\n };\n return {\n step: addStep<\n ResolvedStepType,\n Statement,\n Dependencies,\n Variables,\n GivenState,\n WhenState,\n ThenState,\n RestrictedGivenState,\n RestrictedWhenState,\n RestrictedThenState\n >(statement, stepType, fullDependencies),\n };\n };\n\nconst thenStatement =\n <ResolvedStepType extends StepType, GivenState, WhenState, ThenState>(\n stepType: ResolvedStepType\n ) =>\n <Statement extends ((...args: [...any]) => string) | string>(\n statement: Statement\n ) => {\n let normalizedStatement: Statement extends string\n ? () => string\n : Statement;\n if (isString(statement)) {\n normalizedStatement = (() => statement) as any;\n } else {\n normalizedStatement = statement as any;\n }\n type NormalizedStatement = typeof normalizedStatement;\n\n type Variables = Statement extends string ? [] : GetFunctionArgs<Statement>;\n const dependencyFunc = thenDependencies<\n NormalizedStatement,\n ResolvedStepType,\n Variables,\n GivenState,\n WhenState,\n ThenState\n >(normalizedStatement, stepType);\n const stepFunc = addStep<\n ResolvedStepType,\n NormalizedStatement,\n EmptyDependencies,\n Variables,\n GivenState,\n WhenState,\n ThenState,\n never,\n never,\n never\n >(normalizedStatement, stepType);\n return {\n dependencies: dependencyFunc,\n step: stepFunc,\n };\n };\n\nexport const thenBuilder = <GivenState, WhenState, ThenState>() => {\n return {\n statement: thenStatement<\"then\", GivenState, WhenState, ThenState>(\"then\"),\n };\n};\n","import _ from \"lodash\";\n\nexport type WorldState<State> = {\n readonly [K in keyof State]?: State[K];\n};\n\nexport type MergeableWorldState<T> = WorldState<T> & {\n merge: (newState: Partial<T>) => void;\n};\n\nfunction mergeCustomizer(objValue: unknown, srcValue: unknown) {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n } else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) {\n throw new Error(\n `Merge would have destroyed previous value ${objValue} with ${srcValue}`\n );\n }\n return objValue;\n}\n\nexport const createMergeableState = <T>(\n state: WorldState<T>\n): MergeableWorldState<T> => {\n return {\n ...state,\n merge: (newState: Partial<T>) => {\n state = _.merge({ ...state }, newState, mergeCustomizer);\n },\n };\n};\n\nexport type MergeableWorld<Given, When, Then> = {\n given: MergeableWorldState<Given>;\n when: MergeableWorldState<When>;\n then: MergeableWorldState<Then>;\n};\n\nexport class BasicWorld<Given, When, Then> {\n private givenState: WorldState<Given> = {};\n private whenState: WorldState<When> = {};\n private thenState: WorldState<Then> = {};\n\n public get given(): MergeableWorldState<Given> {\n return {\n ...this.givenState,\n merge: (newState: Partial<Given>) => {\n this.givenState = _.merge(\n { ...this.givenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get when(): MergeableWorldState<When> {\n return {\n ...this.whenState,\n merge: (newState: Partial<When>) => {\n this.whenState = _.merge(\n { ...this.whenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get then(): MergeableWorldState<Then> {\n return {\n ...this.thenState,\n merge: (newState: Partial<Then>) => {\n this.thenState = _.merge(\n { ...this.thenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n}\n\nexport const createBasicWorld = <Given, When, Then>(): MergeableWorld<\n Given,\n When,\n Then\n> => {\n return new BasicWorld<Given, When, Then>();\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\nexport function extractStepDefinitions(\n filePaths: string[],\n tsConfigPath?: string\n): StepDefinitionMeta[] {\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\n const program = ts.createProgram(filePaths, compilerOptions);\n const checker = program.getTypeChecker();\n const results: StepDefinitionMeta[] = [];\n\n for (const filePath of filePaths) {\n const sourceFile = program.getSourceFile(filePath);\n if (!sourceFile) continue;\n const fileResults = extractFromSourceFile(sourceFile, checker);\n results.push(...fileResults);\n }\n\n return results;\n}\n\nfunction extractFromSourceFile(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker\n): StepDefinitionMeta[] {\n const results: StepDefinitionMeta[] = [];\n\n function visit(node: ts.Node) {\n // Look for .register() call expressions\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === \"register\"\n ) {\n const meta = extractFromRegisterCall(node, sourceFile, checker);\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 checker: ts.TypeChecker\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, checker);\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, checker);\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 while (true) {\n // Walk through PropertyAccessExpression to find the next call\n if (ts.isPropertyAccessExpression(current)) {\n current = current.expression;\n }\n\n if (ts.isCallExpression(current)) {\n chain.push(current);\n current = current.expression;\n } else {\n break;\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 (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) {\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))\n 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 checker: ts.TypeChecker\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, checker);\n }\n\n return [];\n}\n\nfunction extractProducedKeysFromCallback(\n callback: ts.ArrowFunction | ts.FunctionExpression,\n checker: ts.TypeChecker\n): string[] {\n const body = callback.body;\n\n // Concise arrow: () => ({ key: value })\n if (!ts.isBlock(body)) {\n return extractKeysFromExpression(body, checker);\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, checker)) {\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 checker: ts.TypeChecker\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 // Try type checker as fallback\n try {\n const type = 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 checker: ts.TypeChecker\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 const symbol = 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 { GherkinClassicTokenMatcher, Parser, AstBuilder } 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\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\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\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(child.scenario, featureBackground, filePath)\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds\n const ruleBackground: messages.Step[] = [...featureBackground];\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(ruleChild.scenario, ruleBackground, filePath)\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string\n): ParsedScenario[] {\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 steps: allSteps,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row\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\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([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: `${scenario.name} (${values.join(\", \")})`,\n file: filePath,\n steps: allSteps,\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 // matching the Step Forge runtime behavior where all params use {string}\n // and values are coerced at runtime via typeCoercer.\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(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): 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(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): 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 =\n \"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(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): 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 { glob } from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { extractStepDefinitions } from \"./stepExtractor.js\";\nimport { parseFeatureFiles, parseFeatureContent } from \"./gherkinParser.js\";\nimport {\n matchScenarioSteps,\n findMatchingDefinitions,\n} 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 resolveGlobs(config.stepFiles);\n const featureFilePaths = await resolveGlobs(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\nasync function resolveGlobs(patterns: string[]): Promise<string[]> {\n const files: string[] = [];\n for (const pattern of patterns) {\n for await (const file of glob(pattern)) {\n files.push(path.resolve(file));\n }\n }\n // Deduplicate\n return [...new Set(files)];\n}\n","import { givenBuilder } from \"./given\";\nimport { whenBuilder } from \"./when\";\nimport { thenBuilder } from \"./then\";\nimport { BasicWorld } from \"./world\";\nimport {\n analyze,\n extractStepDefinitions,\n parseFeatureFiles,\n parseFeatureContent,\n matchScenarioSteps,\n findMatchingDefinitions,\n defaultRules,\n} from \"./analyzer/index\";\nimport type {\n AnalyzerConfig,\n AnalyzeOptions,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n ParsedStep,\n MatchedStep,\n} from \"./analyzer/index\";\n\nexport { givenBuilder, whenBuilder, thenBuilder, BasicWorld };\n\nexport const analyzer = {\n analyze,\n extractStepDefinitions,\n parseFeatureFiles,\n parseFeatureContent,\n matchScenarioSteps,\n findMatchingDefinitions,\n defaultRules,\n};\n\nexport type {\n AnalyzerConfig,\n AnalyzeOptions,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n ParsedStep,\n MatchedStep,\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@step-forge/step-forge",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "module": "./dist/step-forge.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "types": "./dist/step-forge.d.ts",
29
29
  "scripts": {
30
- "build": "rm -rf build && tsc --noEmit && tsdown && cp package.json build/",
30
+ "build": "rm -rf build && tsc --noEmit && tsdown && cp package.json README.md LICENSE.md CHANGELOG.md build/",
31
+ "prepublishOnly": "npm run build",
31
32
  "test:cucumber": "NODE_ENV=test NODE_OPTIONS='--import tsx' cucumber-js -p default",
32
33
  "test:ci": "NODE_ENV=test NODE_OPTIONS='--import tsx' cucumber-js -p ci",
33
34
  "test": "NODE_ENV=test PORT=7888 LOG_LEVEL=none NODE_OPTIONS='--import tsx' cucumber-js -p all 2> /dev/null",
@@ -1,18 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(NODE_ENV=test NODE_OPTIONS=\"--experimental-specifier-resolution=node --loader ts-node/esm\" TS_NODE_PROJECT=./tsconfig.cucumber.json cucumber-js:*)",
5
- "Bash(NODE_ENV=test NODE_OPTIONS=\"--experimental-specifier-resolution=node --loader ts-node/esm\" TS_NODE_PROJECT=./tsconfig.cucumber.json npx cucumber-js:*)",
6
- "Bash(npm run test:cucumber:*)",
7
- "Bash(npx tsc:*)",
8
- "Bash(npm run build:*)",
9
- "Bash(git stash:*)",
10
- "Bash(npm install:*)",
11
- "Bash(npm run test:debug:*)",
12
- "Bash(npm test:*)",
13
- "Bash(git -C /Users/ellisande/dev/step-forge log --oneline -10)",
14
- "Bash(git -C /Users/ellisande/dev/step-forge branch:*)",
15
- "Bash(ls:*)"
16
- ]
17
- }
18
- }
package/.eslintignore DELETED
@@ -1,6 +0,0 @@
1
- .history
2
- .husky
3
- .vscode
4
- coverage
5
- dist
6
- node_modules
package/.eslintrc DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "root": true,
3
- "parser": "@typescript-eslint/parser",
4
- "plugins": ["@typescript-eslint", "prettier"],
5
- "extends": [
6
- "eslint:recommended",
7
- "plugin:@typescript-eslint/eslint-recommended",
8
- "plugin:@typescript-eslint/recommended",
9
- "prettier"
10
- ],
11
- "env": {
12
- "browser": true,
13
- "node": true
14
- },
15
- "rules": {
16
- "prettier/prettier": "error"
17
- }
18
- }
package/.prettierignore DELETED
@@ -1,6 +0,0 @@
1
- .history
2
- .husky
3
- .vscode
4
- coverage
5
- dist
6
- node_modules
package/.prettierrc DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "printWidth": 80,
3
- "tabWidth": 2,
4
- "singleQuote": false,
5
- "trailingComma": "es5",
6
- "arrowParens": "avoid",
7
- "bracketSpacing": true,
8
- "useTabs": false,
9
- "endOfLine": "auto",
10
- "singleAttributePerLine": false,
11
- "bracketSameLine": false,
12
- "jsxSingleQuote": false,
13
- "quoteProps": "as-needed",
14
- "semi": true
15
- }
package/CLAUDE.md DELETED
@@ -1,115 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Commands
6
-
7
- ```bash
8
- npm test # Run all tests (Cucumber.js, suppresses stderr)
9
- npm run test:debug # Run all tests with full output (use when debugging failures)
10
- npm run test:cucumber # Run with default Cucumber profile
11
- npm run test:ci # Run with CI profile
12
- npm run build # Full build: clean → tsc typecheck → tsdown (bundle + dts) → copy package.json
13
- npm run lint # ESLint
14
- npm run format # Prettier
15
- ```
16
-
17
- ## Architecture
18
-
19
- Step Forge is a TypeScript library that wraps Cucumber.js with a type-safe builder pattern for step definitions.
20
-
21
- ### Builder Chain
22
-
23
- Each Gherkin phase (given/when/then) has a builder that follows this chain:
24
-
25
- ```
26
- builder<State>().statement(str | fn) → .dependencies?(deps) → .step(fn) → .register()
27
- ```
28
-
29
- - **Statement**: A string or function. Functions define variables via parameters: `(name: string) => \`a user named ${name}\`` — each parameter becomes a `{string}` placeholder in the Gherkin expression.
30
- - **Dependencies**: Declare which keys from other phases' state this step needs. Keys are marked `"required"` or `"optional"`. Required deps are validated at runtime; optional ones may be `undefined`.
31
- - **Step function**: Receives `{ variables, given, when, then }` — only the phases allowed by the builder type are accessible (given steps can't access when/then state).
32
- - **Register**: Calls the corresponding Cucumber.js `Given`/`When`/`Then` to wire everything up.
33
-
34
- ### Phase Restrictions
35
-
36
- - `givenBuilder` — dependencies on `given` only, returns `Partial<GivenState>`
37
- - `whenBuilder` — dependencies on `given` and `when`, returns `Partial<WhenState>`
38
- - `thenBuilder` — dependencies on all three phases, returns `Partial<ThenState> | void`
39
-
40
- ### Key Source Files
41
-
42
- - `src/common.ts` — `addStep()`: core registration that wires parsers, dependency validation, and state merging into Cucumber
43
- - `src/given.ts`, `src/when.ts`, `src/then.ts` — Builder implementations with phase-specific type constraints
44
- - `src/world.ts` — `BasicWorld<Given, When, Then>` with `MergeableWorldState` (lodash deep merge, arrays concatenate)
45
- - `src/builderTypeUtils.ts` — TypeScript utility types driving the builder's type safety
46
- - `src/utils.ts` — `typeCoercer()` for string→typed conversion, `requireFrom{Given,When,Then}()` for runtime dep validation
47
-
48
- ### Testing
49
-
50
- Tests use Cucumber.js itself (self-testing). Feature files in `features/` with step definitions in `features/steps/` exercise the library. Type-safety tests in `test/` use `@ts-expect-error` annotations — they validate at `tsc` compile time, not at runtime.
51
-
52
- #### Test Scripts
53
-
54
- | Script | Profile | Description |
55
- |---|---|---|
56
- | `npm test` | `all` | Run all tests. Suppresses stderr (`2> /dev/null`) for clean output. Use this for normal development. |
57
- | `npm run test:debug` | `all` | Run all tests with full output (stderr included). Use this when a test fails and you need stack traces or error details. |
58
- | `npm run test:cucumber` | `default` | Run tests using the `default` Cucumber profile. Same paths as `all`, but does not set `PORT` or `LOG_LEVEL`. |
59
- | `npm run test:ci` | `ci` | CI-oriented profile. Does not import `src/**/*.ts` (only step defs). Enables `publish`. |
60
-
61
- All profiles run with `parallel: 1` and use `tsx` as the TypeScript loader via `NODE_OPTIONS='--import tsx'`.
62
-
63
- There is no way to run a single test file. To run a subset, use Cucumber tags or modify the feature files temporarily.
64
-
65
- #### Analyzer Tests
66
-
67
- The analyzer has its own test suite under `features/analyzer/` that tests the `analyze()` API against fixture files. The fixtures are **data** — they are read by the analyzer's extractor and parser, not executed by Cucumber.
68
-
69
- ```
70
- features/analyzer/
71
- analyzer.feature ← Test scenarios (run by Cucumber)
72
- fixtures/
73
- steps.ts ← Fixture step definitions (read by extractor as data)
74
- valid-no-deps.feature ← Fixture feature files (read by parser as data)
75
- valid-deps.feature
76
- missing-given-dep.feature
77
- ...
78
- ```
79
-
80
- The `cucumber.mjs` config uses non-recursive path globs (`./features/*.feature`, `./features/analyzer/*.feature`) to ensure fixture files in `features/analyzer/fixtures/` are never executed as tests.
81
-
82
- Step definitions in `features/steps/analyzerSteps.ts` provide these steps:
83
-
84
- - `Given step definitions from {string}` — sets the fixture step file (relative to `fixtures/`)
85
- - `Given a feature file {string}` — sets the fixture feature file (relative to `fixtures/`)
86
- - `When I analyze the files` — calls `analyze()` with the configured files
87
- - `Then there should be no errors` — asserts zero errors
88
- - `Then there should be {int} error/errors` — asserts exact error count
89
- - `Then an error should mention {string}` — asserts an error message contains the substring
90
-
91
- #### Adding New Analyzer Tests
92
-
93
- 1. **If you need new step definition patterns**, add builder calls to `features/analyzer/fixtures/steps.ts`. This file is only parsed by the TypeScript AST extractor — it is never executed, but it must be valid TypeScript that compiles.
94
-
95
- 2. **Create a fixture feature file** in `features/analyzer/fixtures/` that uses the step expressions defined in `steps.ts`. This file is parsed by the Gherkin parser as data — Cucumber never runs it.
96
-
97
- 3. **Add a scenario** to `features/analyzer/analyzer.feature`:
98
- ```gherkin
99
- Scenario: Description of what you're testing
100
- Given step definitions from "steps.ts"
101
- Given a feature file "your-new-fixture.feature"
102
- When I analyze the files
103
- Then there should be no errors
104
- ```
105
- The Background already provides `step definitions from "steps.ts"`, so you only need the `Given a feature file` line in each scenario.
106
-
107
- 4. **Run `npm run test:debug`** to verify. Use `test:debug` instead of `npm test` so you can see error details if something fails.
108
-
109
- ### Build Output
110
-
111
- `tsdown` (configured in `tsdown.config.ts`, powered by rolldown) produces both the JS bundles and the bundled type declarations in one pass: ESM + CJS for the main entry (`dist/step-forge.js` / `.cjs` with `dist/step-forge.d.ts` / `.d.cts`), and ESM for the analyzer library and CLI (`dist/analyzer.js`, `dist/analyzer-cli.js`). Dependencies and `node:` builtins are externalized automatically. The `build/` directory contains the publishable package.
112
-
113
- ## Exports
114
-
115
- `givenBuilder`, `whenBuilder`, `thenBuilder`, `BasicWorld` — all from `src/index.ts`.
package/cucumber.mjs DELETED
@@ -1,39 +0,0 @@
1
- const defaultProfile = {
2
- format: [
3
- process.env.CI || !process.stdout.isTTY ? "progress" : "progress-bar",
4
- "json:./reports/cucumber-json-reports/report.json",
5
- "rerun:./reports/cucumber/@rerun.txt",
6
- "usage:./reports/cucumber/usage.txt",
7
- ],
8
- parallel: 1,
9
- paths: [
10
- "./features/*.feature",
11
- "./features/analyzer/*.feature",
12
- ],
13
- import: [
14
- "./features/steps/**/*.ts",
15
- "./features/steps/*.ts",
16
- "./src/**/*.ts",
17
- ],
18
- strict: false,
19
- };
20
-
21
- const ciProfile = {
22
- format: [
23
- process.env.CI || !process.stdout.isTTY ? "progress" : "progress-bar",
24
- "json:./reports/cucumber-json-reports/report.json",
25
- "rerun:./reports/cucumber/@rerun.txt",
26
- "usage:./reports/cucumber/usage.txt",
27
- ],
28
- parallel: 1,
29
- import: ["./features/steps/**/*.ts", "./features/steps/*.ts"],
30
- strict: false,
31
- publish: true,
32
- };
33
-
34
- const all = {
35
- ...defaultProfile,
36
- };
37
-
38
- export { ciProfile as ci, all };
39
- export default defaultProfile;
Binary file
@@ -1,100 +0,0 @@
1
- # Testing
2
-
3
- All tests use Cucumber.js in a self-testing pattern: feature files in `features/` with step definitions in `features/steps/` exercise the library.
4
-
5
- ## Test Scripts
6
-
7
- | Script | Profile | Description |
8
- | ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
9
- | `npm test` | `all` | Run all tests. Suppresses stderr for clean output. Use for normal development. |
10
- | `npm run test:debug` | `all` | Run all tests with full output (stderr included). Use when a test fails and you need stack traces or error details. |
11
- | `npm run test:cucumber` | `default` | Same paths as `all`, but does not set `PORT` or `LOG_LEVEL`. |
12
- | `npm run test:ci` | `ci` | CI-oriented profile. Does not import `src/**/*.ts` (only step defs). Enables `publish`. |
13
-
14
- All profiles run with `parallel: 1` and use `tsx` as the TypeScript loader.
15
-
16
- There is no way to run a single test file. To run a subset, use Cucumber tags or modify the feature files temporarily.
17
-
18
- ## Directory Structure
19
-
20
- ```
21
- features/
22
- basic.feature ← Builder pattern tests (variables, deps, state)
23
- exported.feature ← Re-exported builder tests
24
- steps/
25
- commonSteps.ts ← Step defs for basic.feature
26
- exportedSteps.ts ← Step defs for exported.feature
27
- analyzerSteps.ts ← Step defs for analyzer tests
28
- world.ts ← World state types
29
- analyzer/
30
- analyzer.feature ← Analyzer test scenarios (run by Cucumber)
31
- fixtures/
32
- steps.ts ← Fixture step definitions (data, NOT run by Cucumber)
33
- valid-no-deps.feature ← Fixture feature files (data, NOT run by Cucumber)
34
- valid-deps.feature
35
- ...
36
- ```
37
-
38
- ## Builder Tests
39
-
40
- `basic.feature` and `exported.feature` test the core builder pattern — step registration, variable extraction, dependency resolution, and state merging. Their step definitions in `features/steps/` use the builders directly and assert on the resulting world state.
41
-
42
- Type-safety tests in `test/` use `@ts-expect-error` annotations and validate at `tsc` compile time, not at runtime.
43
-
44
- ## Analyzer Tests
45
-
46
- The analyzer test suite under `features/analyzer/` tests the `analyze()` API against fixture files. The key distinction: **fixture files are data, not tests**. The analyzer's extractor reads `fixtures/steps.ts` as a TypeScript AST, and the parser reads `fixtures/*.feature` as Gherkin data. Cucumber never executes them.
47
-
48
- The `cucumber.mjs` config uses non-recursive path globs (`./features/*.feature`, `./features/analyzer/*.feature`) so fixture files in `features/analyzer/fixtures/` are excluded from Cucumber's test discovery.
49
-
50
- ### Available Steps
51
-
52
- Step definitions in `features/steps/analyzerSteps.ts` provide:
53
-
54
- | Step | Purpose |
55
- | ----------------------------------------- | ------------------------------------------------------ |
56
- | `Given step definitions from {string}` | Set the fixture step file (relative to `fixtures/`) |
57
- | `Given a feature file {string}` | Set the fixture feature file (relative to `fixtures/`) |
58
- | `When I analyze the files` | Call `analyze()` with the configured files |
59
- | `Then there should be no errors` | Assert zero errors in diagnostics |
60
- | `Then there should be {int} error/errors` | Assert exact error count |
61
- | `Then an error should mention {string}` | Assert an error message contains the substring |
62
-
63
- ### Adding New Analyzer Tests
64
-
65
- 1. **Add step definition patterns** (if needed) to `features/analyzer/fixtures/steps.ts`. This file must be valid TypeScript that compiles, but it is never executed — only parsed by the AST extractor.
66
-
67
- 2. **Create a fixture feature file** in `features/analyzer/fixtures/` using the step expressions from `steps.ts`. This file is parsed by the Gherkin parser as data.
68
-
69
- 3. **Add a scenario** to `features/analyzer/analyzer.feature`:
70
-
71
- ```gherkin
72
- Scenario: Description of what you're testing
73
- Given a feature file "your-new-fixture.feature"
74
- When I analyze the files
75
- Then there should be no errors
76
- ```
77
-
78
- The Background already provides `Given step definitions from "steps.ts"`, so you only need the `Given a feature file` line in each scenario.
79
-
80
- 4. **Run `npm run test:debug`** to verify. Use `test:debug` instead of `npm test` so you can see error details if something fails.
81
-
82
- ### Fixture Step Definitions
83
-
84
- `features/analyzer/fixtures/steps.ts` contains builder calls covering these patterns:
85
-
86
- | Expression | Type | Dependencies | Produces |
87
- | ----------------------------- | ----- | ------------------------- | --------- |
88
- | `a user` | given | none | `user` |
89
- | `a user named {string}` | given | none | `user` |
90
- | `an account` | given | none | `account` |
91
- | `I started` | given | none | (nothing) |
92
- | `I save the user` | when | `given.user: required` | `user` |
93
- | `I delete the account` | when | `given.account: required` | `result` |
94
- | `I got here` | when | none | (nothing) |
95
- | `everything was good` | then | none | (nothing) |
96
- | `there is a user` | then | `when.user: required` | (nothing) |
97
- | `the user's name is {string}` | then | `when.user: required` | (nothing) |
98
- | `the account might exist` | then | `given.account: optional` | (nothing) |
99
-
100
- When adding new patterns, add them to this file and they'll be available to all fixture feature files.
@@ -1,110 +0,0 @@
1
- Feature: Analyzer dependency verification
2
-
3
- Background:
4
- Given step definitions from "steps.ts"
5
-
6
- # --- Passing scenarios ---
7
-
8
- Scenario: No dependencies produces no errors
9
- Given a feature file "valid-no-deps.feature"
10
- When I analyze the files
11
- Then there should be no errors
12
-
13
- Scenario: Satisfied required dependencies produce no errors
14
- Given a feature file "valid-deps.feature"
15
- When I analyze the files
16
- Then there should be no errors
17
-
18
- Scenario: Variables with satisfied dependencies produce no errors
19
- Given a feature file "valid-variables.feature"
20
- When I analyze the files
21
- Then there should be no errors
22
-
23
- Scenario: Optional dependencies not produced produce no errors
24
- Given a feature file "valid-optional-deps.feature"
25
- When I analyze the files
26
- Then there should be no errors
27
-
28
- Scenario: And/But keywords resolve correctly with satisfied deps
29
- Given a feature file "valid-and-but-keywords.feature"
30
- When I analyze the files
31
- Then there should be no errors
32
-
33
- # --- Undefined step scenarios ---
34
-
35
- Scenario: Undefined step reports an error
36
- Given a feature file "undefined-step.feature"
37
- When I analyze the files
38
- Then there should be 1 error
39
- And there is 1 error for rule "undefined-step"
40
- And an error should mention "I do something that does not exist"
41
- And an error should mention "does not match any step definition"
42
-
43
- Scenario: Undefined step combined with missing dependency
44
- Given a feature file "undefined-with-missing-dep.feature"
45
- When I analyze the files
46
- Then there should be 2 errors
47
- And there is 1 error for rule "undefined-step"
48
- And an error should mention "I do something that does not exist"
49
- And there is 1 error for rule "dependency-check"
50
- And an error should mention "given.user"
51
-
52
- # --- Ambiguous step scenarios ---
53
-
54
- Scenario: Ambiguous step reports an error
55
- Given a feature file "ambiguous-step.feature"
56
- When I analyze the files
57
- Then there should be 1 error
58
- And there is 1 error for rule "ambiguous-step"
59
- And an error should mention "matches multiple step definitions"
60
-
61
- # --- Failing scenarios ---
62
-
63
- Scenario: Missing required given dependency reports an error
64
- Given a feature file "missing-given-dep.feature"
65
- When I analyze the files
66
- Then there should be 1 error
67
- And there is 1 error for rule "dependency-check"
68
- And an error should mention "given.user"
69
-
70
- Scenario: Missing required when dependency reports an error
71
- Given a feature file "missing-when-dep.feature"
72
- When I analyze the files
73
- Then there should be 1 error
74
- And there is 1 error for rule "dependency-check"
75
- And an error should mention "when.user"
76
-
77
- Scenario: Multiple missing dependencies reports multiple errors
78
- Given a feature file "missing-multiple-deps.feature"
79
- When I analyze the files
80
- Then there should be 2 errors
81
- And there are 2 errors for rule "dependency-check"
82
- And an error should mention "given.user"
83
- And an error should mention "given.account"
84
-
85
- # --- Diagnostic range scenarios ---
86
-
87
- Scenario: Undefined step diagnostic range covers only the step text
88
- Given a feature file "undefined-step.feature"
89
- When I analyze the files
90
- Then there should be 1 error
91
- And the error on line 4 should span columns 10 to 44
92
-
93
- Scenario: Ambiguous step diagnostic range covers only the step text
94
- Given a feature file "ambiguous-step.feature"
95
- When I analyze the files
96
- Then there should be 1 error
97
- And the error on line 3 should span columns 11 to 30
98
-
99
- Scenario: Missing dependency diagnostic range covers only the step text
100
- Given a feature file "missing-given-dep.feature"
101
- When I analyze the files
102
- Then there should be 1 error
103
- And the error on line 4 should span columns 10 to 25
104
-
105
- Scenario: Each diagnostic range is correct with multiple errors
106
- Given a feature file "missing-multiple-deps.feature"
107
- When I analyze the files
108
- Then there should be 2 errors
109
- And the error on line 4 should span columns 10 to 25
110
- And the error on line 5 should span columns 10 to 30
@@ -1,5 +0,0 @@
1
- Feature: Ambiguous step
2
- Scenario: Uses an ambiguous step
3
- Given the system is ready
4
- When I got here
5
- Then everything was good
@@ -1,5 +0,0 @@
1
- Feature: Missing given dependency
2
-
3
- Scenario: When step needs given.user but nothing produces it
4
- When I save the user
5
- Then everything was good
@@ -1,6 +0,0 @@
1
- Feature: Multiple missing dependencies
2
-
3
- Scenario: Multiple steps with missing deps
4
- When I save the user
5
- When I delete the account
6
- Then there is a user
@@ -1,5 +0,0 @@
1
- Feature: Missing when dependency
2
-
3
- Scenario: Then step needs when.user but nothing produces it
4
- Given a user
5
- Then there is a user