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