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