@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.
- package/dist/analyzer-QH03uejK.js +478 -0
- package/dist/analyzer-QH03uejK.js.map +1 -0
- package/dist/analyzer-cli.d.ts +1 -0
- package/dist/analyzer-cli.js +69 -0
- package/dist/analyzer-cli.js.map +1 -0
- package/dist/analyzer.d.ts +73 -0
- package/dist/analyzer.js +2 -0
- package/dist/step-forge.cjs +727 -0
- package/dist/step-forge.cjs.map +1 -0
- package/dist/step-forge.d.cts +326 -0
- package/dist/step-forge.d.ts +326 -0
- package/dist/step-forge.js +695 -0
- package/dist/step-forge.js.map +1 -0
- package/package.json +3 -2
- package/.claude/settings.local.json +0 -18
- package/.eslintignore +0 -6
- package/.eslintrc +0 -18
- package/.prettierignore +0 -6
- package/.prettierrc +0 -15
- package/CLAUDE.md +0 -115
- package/cucumber.mjs +0 -39
- package/docs/assets/state_deps.gif +0 -0
- package/features/TESTING.md +0 -100
- package/features/analyzer/analyzer.feature +0 -110
- package/features/analyzer/fixtures/ambiguous-step.feature +0 -5
- package/features/analyzer/fixtures/missing-given-dep.feature +0 -5
- package/features/analyzer/fixtures/missing-multiple-deps.feature +0 -6
- package/features/analyzer/fixtures/missing-when-dep.feature +0 -5
- package/features/analyzer/fixtures/steps.ts +0 -113
- package/features/analyzer/fixtures/undefined-step.feature +0 -5
- package/features/analyzer/fixtures/undefined-with-missing-dep.feature +0 -5
- package/features/analyzer/fixtures/valid-and-but-keywords.feature +0 -8
- package/features/analyzer/fixtures/valid-deps.feature +0 -6
- package/features/analyzer/fixtures/valid-no-deps.feature +0 -6
- package/features/analyzer/fixtures/valid-optional-deps.feature +0 -6
- package/features/analyzer/fixtures/valid-variables.feature +0 -6
- package/features/basic.feature +0 -21
- package/features/exported.feature +0 -6
- package/features/steps/analyzerSteps.ts +0 -67
- package/features/steps/commonSteps.ts +0 -93
- package/features/steps/exportedSteps.ts +0 -42
- package/features/steps/world.ts +0 -29
- package/src/analyzer/cli.ts +0 -96
- package/src/analyzer/gherkinParser.ts +0 -179
- package/src/analyzer/index.ts +0 -89
- package/src/analyzer/rules/ambiguousStepRule.ts +0 -36
- package/src/analyzer/rules/dependencyRule.ts +0 -71
- package/src/analyzer/rules/index.ts +0 -23
- package/src/analyzer/rules/undefinedStepRule.ts +0 -31
- package/src/analyzer/stepExtractor.ts +0 -432
- package/src/analyzer/stepMatcher.ts +0 -85
- package/src/analyzer/types.ts +0 -55
- package/src/builderTypeUtils.ts +0 -27
- package/src/common.ts +0 -138
- package/src/given.ts +0 -102
- package/src/index.ts +0 -46
- package/src/then.ts +0 -128
- package/src/utils.ts +0 -74
- package/src/when.ts +0 -118
- package/src/world.ts +0 -90
- package/test/givenCompilationTests.ts +0 -130
- package/test/testUtils.ts +0 -30
- package/test/thenCompilationTests.ts +0 -207
- package/test/whenCompilationTests.ts +0 -157
- package/ts-node-esm-register.js +0 -8
- package/tsconfig.cucumber.json +0 -14
- package/tsconfig.json +0 -29
- 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.
|
|
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
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
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
|
package/features/TESTING.md
DELETED
|
@@ -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
|