@step-forge/step-forge 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/analyzer-QH03uejK.js +478 -0
  2. package/dist/analyzer-QH03uejK.js.map +1 -0
  3. package/dist/analyzer-cli.d.ts +1 -0
  4. package/dist/analyzer-cli.js +69 -0
  5. package/dist/analyzer-cli.js.map +1 -0
  6. package/dist/analyzer.d.ts +73 -0
  7. package/dist/analyzer.js +2 -0
  8. package/dist/step-forge.cjs +727 -0
  9. package/dist/step-forge.cjs.map +1 -0
  10. package/dist/step-forge.d.cts +326 -0
  11. package/dist/step-forge.d.ts +326 -0
  12. package/dist/step-forge.js +695 -0
  13. package/dist/step-forge.js.map +1 -0
  14. package/package.json +1 -3
  15. package/.claude/settings.local.json +0 -18
  16. package/.eslintignore +0 -6
  17. package/.eslintrc +0 -18
  18. package/.prettierignore +0 -6
  19. package/.prettierrc +0 -15
  20. package/CLAUDE.md +0 -115
  21. package/cucumber.mjs +0 -39
  22. package/docs/assets/state_deps.gif +0 -0
  23. package/features/TESTING.md +0 -100
  24. package/features/analyzer/analyzer.feature +0 -110
  25. package/features/analyzer/fixtures/ambiguous-step.feature +0 -5
  26. package/features/analyzer/fixtures/missing-given-dep.feature +0 -5
  27. package/features/analyzer/fixtures/missing-multiple-deps.feature +0 -6
  28. package/features/analyzer/fixtures/missing-when-dep.feature +0 -5
  29. package/features/analyzer/fixtures/steps.ts +0 -113
  30. package/features/analyzer/fixtures/undefined-step.feature +0 -5
  31. package/features/analyzer/fixtures/undefined-with-missing-dep.feature +0 -5
  32. package/features/analyzer/fixtures/valid-and-but-keywords.feature +0 -8
  33. package/features/analyzer/fixtures/valid-deps.feature +0 -6
  34. package/features/analyzer/fixtures/valid-no-deps.feature +0 -6
  35. package/features/analyzer/fixtures/valid-optional-deps.feature +0 -6
  36. package/features/analyzer/fixtures/valid-variables.feature +0 -6
  37. package/features/basic.feature +0 -21
  38. package/features/exported.feature +0 -6
  39. package/features/steps/analyzerSteps.ts +0 -67
  40. package/features/steps/commonSteps.ts +0 -93
  41. package/features/steps/exportedSteps.ts +0 -42
  42. package/features/steps/world.ts +0 -29
  43. package/src/analyzer/cli.ts +0 -96
  44. package/src/analyzer/gherkinParser.ts +0 -179
  45. package/src/analyzer/index.ts +0 -89
  46. package/src/analyzer/rules/ambiguousStepRule.ts +0 -36
  47. package/src/analyzer/rules/dependencyRule.ts +0 -71
  48. package/src/analyzer/rules/index.ts +0 -23
  49. package/src/analyzer/rules/undefinedStepRule.ts +0 -31
  50. package/src/analyzer/stepExtractor.ts +0 -432
  51. package/src/analyzer/stepMatcher.ts +0 -85
  52. package/src/analyzer/types.ts +0 -55
  53. package/src/builderTypeUtils.ts +0 -27
  54. package/src/common.ts +0 -138
  55. package/src/given.ts +0 -102
  56. package/src/index.ts +0 -46
  57. package/src/then.ts +0 -128
  58. package/src/utils.ts +0 -74
  59. package/src/when.ts +0 -118
  60. package/src/world.ts +0 -90
  61. package/test/givenCompilationTests.ts +0 -130
  62. package/test/testUtils.ts +0 -30
  63. package/test/thenCompilationTests.ts +0 -207
  64. package/test/whenCompilationTests.ts +0 -157
  65. package/ts-node-esm-register.js +0 -8
  66. package/tsconfig.cucumber.json +0 -14
  67. package/tsconfig.json +0 -29
  68. package/tsdown.config.ts +0 -35
@@ -1,179 +0,0 @@
1
- import * as fs from "node:fs";
2
- import { GherkinClassicTokenMatcher, Parser, AstBuilder } from "@cucumber/gherkin";
3
- import * as messages from "@cucumber/messages";
4
- import { ParsedScenario, ParsedStep } from "./types.js";
5
-
6
- type GherkinKeyword = "Given" | "When" | "Then" | "And" | "But";
7
-
8
- export function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {
9
- const scenarios: ParsedScenario[] = [];
10
-
11
- for (const filePath of filePaths) {
12
- const content = fs.readFileSync(filePath, "utf-8");
13
- const parsed = parseFeatureContent(content, filePath);
14
- scenarios.push(...parsed);
15
- }
16
-
17
- return scenarios;
18
- }
19
-
20
- export function parseFeatureContent(
21
- content: string,
22
- filePath: string
23
- ): ParsedScenario[] {
24
- const newId = messages.IdGenerator.uuid();
25
- const builder = new AstBuilder(newId);
26
- const matcher = new GherkinClassicTokenMatcher();
27
- const parser = new Parser(builder, matcher);
28
-
29
- const gherkinDocument: messages.GherkinDocument = parser.parse(content);
30
- const feature = gherkinDocument.feature;
31
- if (!feature) return [];
32
-
33
- // Collect background steps at the feature level
34
- const featureBackground: messages.Step[] = [];
35
- const scenarios: ParsedScenario[] = [];
36
-
37
- for (const child of feature.children) {
38
- if (child.background) {
39
- featureBackground.push(...child.background.steps);
40
- }
41
-
42
- if (child.scenario) {
43
- scenarios.push(
44
- ...expandScenario(child.scenario, featureBackground, filePath)
45
- );
46
- }
47
-
48
- if (child.rule) {
49
- // Rules can have their own backgrounds
50
- const ruleBackground: messages.Step[] = [...featureBackground];
51
- for (const ruleChild of child.rule.children) {
52
- if (ruleChild.background) {
53
- ruleBackground.push(...ruleChild.background.steps);
54
- }
55
- if (ruleChild.scenario) {
56
- scenarios.push(
57
- ...expandScenario(ruleChild.scenario, ruleBackground, filePath)
58
- );
59
- }
60
- }
61
- }
62
- }
63
-
64
- return scenarios;
65
- }
66
-
67
- function expandScenario(
68
- scenario: messages.Scenario,
69
- backgroundSteps: messages.Step[],
70
- filePath: string
71
- ): ParsedScenario[] {
72
- const hasExamples =
73
- scenario.examples.length > 0 &&
74
- scenario.examples.some((e) => e.tableBody.length > 0);
75
-
76
- if (!hasExamples) {
77
- // Regular scenario
78
- const bgParsed = convertSteps(backgroundSteps);
79
- const scenarioParsed = convertSteps(scenario.steps);
80
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
81
-
82
- return [
83
- {
84
- name: scenario.name,
85
- file: filePath,
86
- steps: allSteps,
87
- },
88
- ];
89
- }
90
-
91
- // Scenario Outline — expand with each example row
92
- const results: ParsedScenario[] = [];
93
- for (const example of scenario.examples) {
94
- if (!example.tableHeader || example.tableBody.length === 0) continue;
95
- const headers = example.tableHeader.cells.map((c) => c.value);
96
-
97
- for (const row of example.tableBody) {
98
- const values = row.cells.map((c) => c.value);
99
- const substitution: Record<string, string> = {};
100
- headers.forEach((h, i) => {
101
- substitution[h] = values[i];
102
- });
103
-
104
- const bgParsed = convertSteps(backgroundSteps);
105
- const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
106
- ...step,
107
- text: substituteExampleValues(step.text, substitution),
108
- }));
109
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
110
-
111
- results.push({
112
- name: `${scenario.name} (${values.join(", ")})`,
113
- file: filePath,
114
- steps: allSteps,
115
- });
116
- }
117
- }
118
-
119
- return results;
120
- }
121
-
122
- function convertSteps(
123
- steps: readonly messages.Step[]
124
- ): Omit<ParsedStep, "effectiveKeyword">[] {
125
- return steps.map((step) => ({
126
- keyword: normalizeKeyword(step.keyword),
127
- text: step.text,
128
- line: step.location.line,
129
- // step.location.column points to the keyword start; shift past the
130
- // keyword (which includes a trailing space) so column points to the
131
- // start of the step text. This makes `column + text.length` produce
132
- // the correct end position for diagnostic ranges.
133
- column: (step.location.column ?? 1) + step.keyword.length,
134
- }));
135
- }
136
-
137
- function normalizeKeyword(keyword: string): GherkinKeyword {
138
- const trimmed = keyword.trim();
139
- // Gherkin keywords may include trailing space, e.g. "Given "
140
- if (trimmed === "Given") return "Given";
141
- if (trimmed === "When") return "When";
142
- if (trimmed === "Then") return "Then";
143
- if (trimmed === "And") return "And";
144
- if (trimmed === "But") return "But";
145
- // Fallback: treat as Given (shouldn't happen with valid Gherkin)
146
- return "Given";
147
- }
148
-
149
- function resolveEffectiveKeywords(
150
- steps: Omit<ParsedStep, "effectiveKeyword">[]
151
- ): ParsedStep[] {
152
- let lastEffective: "Given" | "When" | "Then" = "Given";
153
-
154
- return steps.map((step) => {
155
- let effectiveKeyword: "Given" | "When" | "Then";
156
- if (step.keyword === "And" || step.keyword === "But") {
157
- effectiveKeyword = lastEffective;
158
- } else {
159
- effectiveKeyword = step.keyword as "Given" | "When" | "Then";
160
- }
161
- lastEffective = effectiveKeyword;
162
-
163
- return {
164
- ...step,
165
- effectiveKeyword,
166
- };
167
- });
168
- }
169
-
170
- function substituteExampleValues(
171
- text: string,
172
- substitution: Record<string, string>
173
- ): string {
174
- let result = text;
175
- for (const [key, value] of Object.entries(substitution)) {
176
- result = result.replace(new RegExp(`<${key}>`, "g"), value);
177
- }
178
- return result;
179
- }
@@ -1,89 +0,0 @@
1
- import { glob } from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { extractStepDefinitions } from "./stepExtractor.js";
4
- import { parseFeatureFiles, parseFeatureContent } from "./gherkinParser.js";
5
- import {
6
- matchScenarioSteps,
7
- findMatchingDefinitions,
8
- } from "./stepMatcher.js";
9
- import { defaultRules, runRules } from "./rules/index.js";
10
- import type {
11
- AnalyzerConfig,
12
- AnalysisRule,
13
- Diagnostic,
14
- StepDefinitionMeta,
15
- ParsedScenario,
16
- MatchedStep,
17
- ParsedStep,
18
- } from "./types.js";
19
-
20
- export type {
21
- AnalyzerConfig,
22
- AnalysisRule,
23
- Diagnostic,
24
- StepDefinitionMeta,
25
- ParsedScenario,
26
- MatchedStep,
27
- ParsedStep,
28
- };
29
-
30
- export {
31
- extractStepDefinitions,
32
- parseFeatureFiles,
33
- parseFeatureContent,
34
- matchScenarioSteps,
35
- findMatchingDefinitions,
36
- defaultRules,
37
- };
38
-
39
- export interface AnalyzeOptions {
40
- rules?: AnalysisRule[];
41
- }
42
-
43
- export async function analyze(
44
- config: AnalyzerConfig,
45
- options?: AnalyzeOptions
46
- ): Promise<Diagnostic[]> {
47
- const rules = options?.rules ?? defaultRules;
48
-
49
- // 1. Resolve file globs to paths
50
- const stepFilePaths = await resolveGlobs(config.stepFiles);
51
- const featureFilePaths = await resolveGlobs(config.featureFiles);
52
-
53
- if (stepFilePaths.length === 0) {
54
- return [];
55
- }
56
- if (featureFilePaths.length === 0) {
57
- return [];
58
- }
59
-
60
- // 2. Extract step metadata from step files
61
- const stepDefinitions = extractStepDefinitions(
62
- stepFilePaths,
63
- config.tsConfigPath
64
- );
65
-
66
- // 3. Parse feature files
67
- const scenarios = parseFeatureFiles(featureFilePaths);
68
-
69
- // 4. For each scenario, match steps and run rules
70
- const diagnostics: Diagnostic[] = [];
71
- for (const scenario of scenarios) {
72
- const matchedSteps = matchScenarioSteps(scenario, stepDefinitions);
73
- const scenarioDiags = runRules(rules, scenario, matchedSteps);
74
- diagnostics.push(...scenarioDiags);
75
- }
76
-
77
- return diagnostics;
78
- }
79
-
80
- async function resolveGlobs(patterns: string[]): Promise<string[]> {
81
- const files: string[] = [];
82
- for (const pattern of patterns) {
83
- for await (const file of glob(pattern)) {
84
- files.push(path.resolve(file));
85
- }
86
- }
87
- // Deduplicate
88
- return [...new Set(files)];
89
- }
@@ -1,36 +0,0 @@
1
- import {
2
- AnalysisRule,
3
- Diagnostic,
4
- MatchedStep,
5
- ParsedScenario,
6
- } from "../types.js";
7
-
8
- export const ambiguousStepRule: AnalysisRule = {
9
- name: "ambiguous-step",
10
-
11
- check(
12
- scenario: ParsedScenario,
13
- matchedSteps: MatchedStep[]
14
- ): Diagnostic[] {
15
- return matchedSteps
16
- .filter((step) => step.definitions.length > 1)
17
- .map((step) => {
18
- const locations = step.definitions
19
- .map((d) => `${d.sourceFile}:${d.line}`)
20
- .join(", ");
21
- return {
22
- file: scenario.file,
23
- range: {
24
- startLine: step.line,
25
- startColumn: step.column,
26
- endLine: step.line,
27
- endColumn: step.column + step.text.length,
28
- },
29
- severity: "error" as const,
30
- message: `Step "${step.text}" matches multiple step definitions: ${locations}`,
31
- rule: "ambiguous-step",
32
- source: "step-forge" as const,
33
- };
34
- });
35
- },
36
- };
@@ -1,71 +0,0 @@
1
- import {
2
- AnalysisRule,
3
- Diagnostic,
4
- MatchedStep,
5
- ParsedScenario,
6
- } from "../types.js";
7
-
8
- export const dependencyRule: AnalysisRule = {
9
- name: "dependency-check",
10
-
11
- check(
12
- scenario: ParsedScenario,
13
- matchedSteps: MatchedStep[]
14
- ): Diagnostic[] {
15
- const diagnostics: Diagnostic[] = [];
16
- const produced = {
17
- given: new Set<string>(),
18
- when: new Set<string>(),
19
- then: new Set<string>(),
20
- };
21
-
22
- for (const step of matchedSteps) {
23
- if (step.definitions.length !== 1) continue;
24
-
25
- const { dependencies, produces, stepType } = step.definitions[0];
26
-
27
- // Collect all missing required dependencies for this step
28
- const missing: Record<string, string[]> = {};
29
- for (const phase of ["given", "when", "then"] as const) {
30
- for (const [key, requirement] of Object.entries(dependencies[phase])) {
31
- if (requirement === "required" && !produced[phase].has(key)) {
32
- if (!missing[phase]) {
33
- missing[phase] = [];
34
- }
35
- missing[phase].push(key);
36
- }
37
- }
38
- }
39
-
40
- if (Object.keys(missing).length > 0) {
41
- const lines = Object.entries(missing).map(
42
- ([phase, keys]) =>
43
- `${phase.charAt(0).toUpperCase() + phase.slice(1)}: ${keys.map((k) => `${phase}.${k}`).join(", ")}`
44
- );
45
- const message =
46
- "Missing required dependencies:\n" + lines.join("\n");
47
-
48
- diagnostics.push({
49
- file: scenario.file,
50
- range: {
51
- startLine: step.line,
52
- startColumn: step.column,
53
- endLine: step.line,
54
- endColumn: step.column + step.text.length,
55
- },
56
- severity: "error",
57
- message,
58
- rule: "dependency-check",
59
- source: "step-forge",
60
- });
61
- }
62
-
63
- // Add produced keys for this step
64
- for (const key of produces) {
65
- produced[stepType].add(key);
66
- }
67
- }
68
-
69
- return diagnostics;
70
- },
71
- };
@@ -1,23 +0,0 @@
1
- import {
2
- AnalysisRule,
3
- Diagnostic,
4
- MatchedStep,
5
- ParsedScenario,
6
- } from "../types.js";
7
- import { ambiguousStepRule } from "./ambiguousStepRule.js";
8
- import { dependencyRule } from "./dependencyRule.js";
9
- import { undefinedStepRule } from "./undefinedStepRule.js";
10
-
11
- export const defaultRules: AnalysisRule[] = [
12
- undefinedStepRule,
13
- ambiguousStepRule,
14
- dependencyRule,
15
- ];
16
-
17
- export function runRules(
18
- rules: AnalysisRule[],
19
- scenario: ParsedScenario,
20
- matchedSteps: MatchedStep[]
21
- ): Diagnostic[] {
22
- return rules.flatMap((rule) => rule.check(scenario, matchedSteps));
23
- }
@@ -1,31 +0,0 @@
1
- import {
2
- AnalysisRule,
3
- Diagnostic,
4
- MatchedStep,
5
- ParsedScenario,
6
- } from "../types.js";
7
-
8
- export const undefinedStepRule: AnalysisRule = {
9
- name: "undefined-step",
10
-
11
- check(
12
- scenario: ParsedScenario,
13
- matchedSteps: MatchedStep[]
14
- ): Diagnostic[] {
15
- return matchedSteps
16
- .filter((step) => step.definitions.length === 0)
17
- .map((step) => ({
18
- file: scenario.file,
19
- range: {
20
- startLine: step.line,
21
- startColumn: step.column,
22
- endLine: step.line,
23
- endColumn: step.column + step.text.length,
24
- },
25
- severity: "error" as const,
26
- message: `Step "${step.text}" does not match any step definition`,
27
- rule: "undefined-step",
28
- source: "step-forge" as const,
29
- }));
30
- },
31
- };