From 2ae4f3d5156b0bfe5595d0bb729ac0d1a9bd9f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Thu, 11 Dec 2025 13:30:38 +0200 Subject: [PATCH 1/9] update lockfile --- package-lock.json | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ef890a4..87f0398c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -512,6 +512,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", "dev": true, + "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" } @@ -521,6 +522,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-30.0.0.tgz", "integrity": "sha512-Q49dy57Iv58sTU4z3I9FU0mfCOk/6akuqiANr99eWzX2hBuywbsrGSwi6nEAisLJwiKvSK/kKxlo/YQPegjkEA==", "license": "MIT", + "peer": true, "dependencies": { "class-transformer": "0.5.1", "reflect-metadata": "0.2.2" @@ -985,6 +987,7 @@ "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1051,6 +1054,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -1225,6 +1229,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2337,6 +2342,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2409,6 +2415,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5490,6 +5497,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6691,18 +6699,6 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, - "node_modules/tree-sitter": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", - "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0" - } - }, "node_modules/tree-sitter-c-sharp": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz", @@ -7158,6 +7154,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From a901818ffd6f7b6a6c89da2eec77d7919a03f6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Tue, 3 Feb 2026 11:50:37 +0200 Subject: [PATCH 2/9] add ExpressionBuilder.rebuild method --- .tool-versions | 1 + src/language/ExpressionBuilder.ts | 39 +++++++++++++++++++++++++++++++ src/language/helpers.ts | 2 +- src/language/types.ts | 6 ++--- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..d0b9920e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24.11.0 \ No newline at end of file diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index 54567a06..8e83aff5 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -67,4 +67,43 @@ export class ExpressionBuilder { registry, } } + + // update existing result with new sources + rebuild( + existingResult: ExpressionBuilderResult, + sources: readonly Source[] + ): ExpressionBuilderResult { + const errors: Error[] = [] + const registry = existingResult.registry + const expressionFactory = new ExpressionFactory(registry) + + const sourceAnalyser = new SourceAnalyzer(this.parserAdapter, sources) + + // TODO: we cant currently override existing parameter type as it raises an error + // const parameterTypeLinks: ParameterTypeLink[] = [] + // sourceAnalyser.eachParameterTypeLink((parameterTypeLink) => { + // defineParameterType(parameterTypeLink.parameterType) + // parameterTypeLinks.push(parameterTypeLink) + // }) + + // extract to private method + sourceAnalyser.eachStepDefinitionExpression( + (stepDefinitionExpression, rootNode, expressionNode, source) => { + try { + const expression = expressionFactory.createExpression(stepDefinitionExpression) + const locationLink = createLocationLink(rootNode, expressionNode, source.uri) + existingResult.expressionLinks.push({ expression, locationLink }) + } catch (err) { + errors.push(err) + } + } + ) + + return { + expressionLinks: sortLinks(existingResult.expressionLinks), + parameterTypeLinks: sortLinks(existingResult.parameterTypeLinks), + errors: sourceAnalyser.getErrors().concat(errors), + registry, + } + } } diff --git a/src/language/helpers.ts b/src/language/helpers.ts index 8d4b6de4..9c397f0c 100644 --- a/src/language/helpers.ts +++ b/src/language/helpers.ts @@ -18,7 +18,7 @@ export function makeParameterType(name: string, regexps: RegExps) { return new ParameterType(name, regexps, Object, (arg) => arg, true, false) } -export function sortLinks(links: L[]): readonly L[] { +export function sortLinks(links: L[]): L[] { return links.sort((a, b) => { const pathComparison = a.locationLink.targetUri.localeCompare(b.locationLink.targetUri) if (pathComparison !== 0) return pathComparison diff --git a/src/language/types.ts b/src/language/types.ts index 7929b75f..bcb6ad69 100644 --- a/src/language/types.ts +++ b/src/language/types.ts @@ -68,9 +68,9 @@ export type Language = Readonly<{ }> export type ExpressionBuilderResult = Readonly<{ - readonly expressionLinks: readonly ExpressionLink[] - readonly parameterTypeLinks: readonly ParameterTypeLink[] - readonly errors: readonly Error[] + readonly expressionLinks: ExpressionLink[] + readonly parameterTypeLinks: ParameterTypeLink[] + readonly errors: Error[] readonly registry: ParameterTypeRegistry }> From e0fa838ff94f337695a35780cbff66106380addc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Wed, 4 Feb 2026 14:40:05 +0200 Subject: [PATCH 3/9] use set for expression builder --- src/messages/MessagesBuilder.ts | 2 +- src/suggestions/buildSuggestions.ts | 2 +- .../buildSuggestionsFromRegularExpression.ts | 2 +- test/suggestions/buildSuggestions.test.ts | 20 +++++++++---------- ...ldSuggestionsFromRegularExpression.test.ts | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index dae02885..6f5be52b 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -64,7 +64,7 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - suggestions: buildSuggestions(this.parameterTypeRegistry, this.stepTexts, this.expressions), + suggestions: buildSuggestions(this.parameterTypeRegistry, new Set(this.stepTexts), this.expressions), expressions: this.expressions, } } diff --git a/src/suggestions/buildSuggestions.ts b/src/suggestions/buildSuggestions.ts index e8823ced..1e75c7fe 100644 --- a/src/suggestions/buildSuggestions.ts +++ b/src/suggestions/buildSuggestions.ts @@ -20,7 +20,7 @@ import { Suggestion } from './types.js' */ export function buildSuggestions( registry: ParameterTypeRegistry, - stepTexts: readonly string[], + stepTexts: Set, expressions: readonly Expression[], maxChoices = 10 ): readonly Suggestion[] { diff --git a/src/suggestions/buildSuggestionsFromRegularExpression.ts b/src/suggestions/buildSuggestionsFromRegularExpression.ts index fd7c38a6..573de4f5 100644 --- a/src/suggestions/buildSuggestionsFromRegularExpression.ts +++ b/src/suggestions/buildSuggestionsFromRegularExpression.ts @@ -5,7 +5,7 @@ import { ParameterChoices, Suggestion, SuggestionSegment } from './types' export function buildSuggestionsFromRegularExpression( expression: RegularExpression, registry: ParameterTypeRegistry, - stepTexts: readonly string[], + stepTexts: Set, parameterChoices: ParameterChoices ): readonly Suggestion[] { const segmentJsons = new Set() diff --git a/test/suggestions/buildSuggestions.test.ts b/test/suggestions/buildSuggestions.test.ts index fb88adb5..2a67daee 100644 --- a/test/suggestions/buildSuggestions.test.ts +++ b/test/suggestions/buildSuggestions.test.ts @@ -17,7 +17,7 @@ describe('buildSuggestions', () => { assertSuggestions( parameterTypeRegistry, - ['The nice song', 'The big boat'], + new Set(['The nice song', 'The big boat']), [e1, e2], [ { @@ -40,11 +40,11 @@ describe('buildSuggestions', () => { const expression = ef.createExpression('I have {int} cukes in/on my {word}') assertSuggestions( parameterTypeRegistry, - [ + new Set([ 'I have 42 cukes in my belly', 'I have 54 cukes on my table', 'I have 54 cukes in my basket', - ], + ]), [expression], [ { @@ -69,7 +69,7 @@ describe('buildSuggestions', () => { const expression = ef.createExpression(/I have (\d\d) cukes in my "(belly|suitcase)"/) assertSuggestions( parameterTypeRegistry, - ['I have 42 cukes in my "belly"', 'I have 54 cukes in my "suitcase"'], + new Set(['I have 42 cukes in my "belly"', 'I have 54 cukes in my "suitcase"']), [expression], [ { @@ -87,7 +87,7 @@ describe('buildSuggestions', () => { const expression = ef.createExpression(/^the price of a "(.*?)" is (\d+)c$/) assertSuggestions( parameterTypeRegistry, - ['the price of a "lemon" is 34c', 'the price of a "pear" is 48c'], + new Set(['the price of a "lemon" is 34c', 'the price of a "pear" is 48c']), [expression], [ { @@ -105,12 +105,12 @@ describe('buildSuggestions', () => { const expression = ef.createExpression('I have {int} cukes in/on my {word}') assertSuggestions( parameterTypeRegistry, - [ + new Set([ 'I have 42 cukes in my belly', 'I have 54 cukes on my table', 'I have 67 cukes in my belly', 'I have 54 cukes in my basket', - ], + ]), [expression], [ { @@ -128,11 +128,11 @@ describe('buildSuggestions', () => { assertSuggestions( parameterTypeRegistry, - [ + new Set([ 'I have 42 cukes in my belly', 'I have 54 cukes on my table', 'I have 54 cukes in my basket', - ], + ]), [], [ { @@ -157,7 +157,7 @@ describe('buildSuggestions', () => { function assertSuggestions( parameterTypeRegistry: ParameterTypeRegistry, - stepTexts: readonly string[], + stepTexts: Set, expressions: readonly Expression[], expectedSuggestions: Suggestion[], maxChoices = 10 diff --git a/test/suggestions/buildSuggestionsFromRegularExpression.test.ts b/test/suggestions/buildSuggestionsFromRegularExpression.test.ts index d8a9d708..2969e4bf 100644 --- a/test/suggestions/buildSuggestionsFromRegularExpression.test.ts +++ b/test/suggestions/buildSuggestionsFromRegularExpression.test.ts @@ -20,7 +20,7 @@ describe('buildSuggestionsFromRegularExpression', () => { const actual = buildSuggestionsFromRegularExpression( expression, registry, - ['I have 4 cukes'], + new Set(['I have 4 cukes']), {} ) assert.deepStrictEqual(actual, [expected]) @@ -33,7 +33,7 @@ describe('buildSuggestionsFromRegularExpression', () => { label: 'I have (\\d+) cukes', matched: true, } - const actual = buildSuggestionsFromRegularExpression(expression, registry, ['I have 4 cukes'], { + const actual = buildSuggestionsFromRegularExpression(expression, registry, new Set(['I have 4 cukes']), { '-?\\d+|\\d+': ['12'], }) assert.deepStrictEqual(actual, [expected]) @@ -49,7 +49,7 @@ describe('buildSuggestionsFromRegularExpression', () => { const actual = buildSuggestionsFromRegularExpression( expression, registry, - ['the price of a "lemon" is 34c'], + new Set(['the price of a "lemon" is 34c']), {} ) assert.deepStrictEqual(actual, [expected]) From 683fc9466784eb398eb7d1e60e452be7000ce046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Thu, 5 Feb 2026 15:47:42 +0200 Subject: [PATCH 4/9] sourcelinks caching --- src/language/ExpressionBuilder.ts | 41 ++++++++++++------- src/language/SourceAnalyzer.ts | 9 ++-- src/language/types.ts | 4 +- test/language/ExpressionBuilder.test.ts | 10 ++--- .../snippet/stepDefinitionSnippet.test.ts | 4 +- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index 8e83aff5..8631770c 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -41,28 +41,33 @@ export class ExpressionBuilder { const sourceAnalyser = new SourceAnalyzer(this.parserAdapter, sources) - const parameterTypeLinks: ParameterTypeLink[] = [] - sourceAnalyser.eachParameterTypeLink((parameterTypeLink) => { + const parameterTypeLinks: Map = new Map() + sourceAnalyser.eachParameterTypeLink((parameterTypeLink, source) => { defineParameterType(parameterTypeLink.parameterType) - parameterTypeLinks.push(parameterTypeLink) + parameterTypeLinks.get(source.uri)?.push(parameterTypeLink) + if (!parameterTypeLinks.get(source.uri)) { + parameterTypeLinks.set(source.uri, [parameterTypeLink]) + } }) - const expressionLinks: ExpressionLink[] = [] + const expressionLinks: Map = new Map() sourceAnalyser.eachStepDefinitionExpression( (stepDefinitionExpression, rootNode, expressionNode, source) => { try { const expression = expressionFactory.createExpression(stepDefinitionExpression) const locationLink = createLocationLink(rootNode, expressionNode, source.uri) - expressionLinks.push({ expression, locationLink }) - } catch (err) { - errors.push(err) - } + expressionLinks.get(source.uri)?.push({ expression, locationLink }) + if (!expressionLinks.get(source.uri)) { + expressionLinks.set(source.uri, [{ expression, locationLink }]) + } + } catch (err) { + errors.push(err) } - ) + }) return { - expressionLinks: sortLinks(expressionLinks), - parameterTypeLinks: sortLinks(parameterTypeLinks), + expressionLinks, + parameterTypeLinks, errors: sourceAnalyser.getErrors().concat(errors), registry, } @@ -87,12 +92,20 @@ export class ExpressionBuilder { // }) // extract to private method + + const cleared = new Map() sourceAnalyser.eachStepDefinitionExpression( (stepDefinitionExpression, rootNode, expressionNode, source) => { + try { const expression = expressionFactory.createExpression(stepDefinitionExpression) const locationLink = createLocationLink(rootNode, expressionNode, source.uri) - existingResult.expressionLinks.push({ expression, locationLink }) + // clear the existing expression links for this source + if (!cleared.get(source.uri)) { + existingResult.expressionLinks.set(source.uri, []) + cleared.set(source.uri, true) + } + existingResult.expressionLinks.get(source.uri)?.push({ expression, locationLink }) } catch (err) { errors.push(err) } @@ -100,8 +113,8 @@ export class ExpressionBuilder { ) return { - expressionLinks: sortLinks(existingResult.expressionLinks), - parameterTypeLinks: sortLinks(existingResult.parameterTypeLinks), + expressionLinks: existingResult.expressionLinks, + parameterTypeLinks: existingResult.parameterTypeLinks, errors: sourceAnalyser.getErrors().concat(errors), registry, } diff --git a/src/language/SourceAnalyzer.ts b/src/language/SourceAnalyzer.ts index ce015de1..845bbb0d 100644 --- a/src/language/SourceAnalyzer.ts +++ b/src/language/SourceAnalyzer.ts @@ -31,7 +31,7 @@ export class SourceAnalyzer { private readonly sources: readonly Source[] ) {} - eachParameterTypeLink(callback: (parameterTypeLink: ParameterTypeLink) => void) { + eachParameterTypeLink(callback: (parameterTypeLink: ParameterTypeLink, source: Source) => void) { const parameterTypeMatches = this.getSourceMatches( (language: Language) => language.defineParameterTypeQueries ) @@ -50,11 +50,11 @@ export class SourceAnalyzer { const locationLink = createLocationLink(rootNode, selectionNode, source.uri) const props: ParameterTypeLinkProps = (propsByName[parameterTypeName] = propsByName[ parameterTypeName - ] || { locationLink, regexpsList: [] }) + ] || { locationLink, regexpsList: [], source }) props.regexpsList.push(regExps) } } - for (const [name, { regexpsList, locationLink }] of Object.entries(propsByName)) { + for (const [name, { regexpsList, locationLink, source }] of Object.entries(propsByName)) { const regexps: StringOrRegExp[] = regexpsList.reduce((prev, current) => { if (Array.isArray(current)) { return prev.concat(...current) @@ -64,7 +64,7 @@ export class SourceAnalyzer { }, []) const parameterType = makeParameterType(name, regexps) const parameterTypeLink: ParameterTypeLink = { parameterType, locationLink } - callback(parameterTypeLink) + callback(parameterTypeLink, source) } } } @@ -143,6 +143,7 @@ language: ${source.languageName} } type ParameterTypeLinkProps = { + source: Source regexpsList: RegExps[] locationLink: LocationLink } diff --git a/src/language/types.ts b/src/language/types.ts index bcb6ad69..dbcea07a 100644 --- a/src/language/types.ts +++ b/src/language/types.ts @@ -68,8 +68,8 @@ export type Language = Readonly<{ }> export type ExpressionBuilderResult = Readonly<{ - readonly expressionLinks: ExpressionLink[] - readonly parameterTypeLinks: ParameterTypeLink[] + readonly expressionLinks: Map + readonly parameterTypeLinks: Map readonly errors: Error[] readonly registry: ParameterTypeRegistry }> diff --git a/test/language/ExpressionBuilder.test.ts b/test/language/ExpressionBuilder.test.ts index f55608de..a8483eb3 100644 --- a/test/language/ExpressionBuilder.test.ts +++ b/test/language/ExpressionBuilder.test.ts @@ -37,7 +37,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = result.parameterTypeLinks.find( + const regexpStrings = Array.from(result.parameterTypeLinks.values()).flat().find( (l) => l.parameterType.name === 'WithoutExpression' )?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['.*']) @@ -47,7 +47,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = result.parameterTypeLinks.find( + const regexpStrings = Array.from(result.parameterTypeLinks.values()).flat().find( (l) => l.parameterType.name === 'DateTime' )?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['today', 'tomorrow', '(.*) days later']) @@ -65,7 +65,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { ]) // verify that the targetSelectionRange is inside the targetRange - for (const link of result.expressionLinks.map((l) => l.locationLink)) { + for (const link of Array.from(result.expressionLinks.values()).flat().map((l) => l.locationLink)) { assert( link.targetSelectionRange.start.line > link.targetRange.start.line || link.targetSelectionRange.start.character >= link.targetRange.start.character @@ -75,7 +75,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { link.targetSelectionRange.end.character <= link.targetRange.end.character ) } - const expressions = result.expressionLinks.map(({ expression }) => + const expressions = Array.from(result.expressionLinks.values()).flat().map(({ expression }) => expression instanceof CucumberExpression ? expression.source : (expression as RegularExpression).regexp @@ -102,7 +102,7 @@ Please register a ParameterType for 'undefined-parameter'`, // Verify that the extracted expressions actually work let matched = false - for (const expressionLink of result.expressionLinks) { + for (const expressionLink of Array.from(result.expressionLinks.values()).flat()) { const match = expressionLink.expression.match('a 2020-12-24') if (match) { assert.strictEqual(match[0].getValue(undefined), '2020-12-24') diff --git a/test/service/snippet/stepDefinitionSnippet.test.ts b/test/service/snippet/stepDefinitionSnippet.test.ts index 1b4c1c93..fe221ed4 100644 --- a/test/service/snippet/stepDefinitionSnippet.test.ts +++ b/test/service/snippet/stepDefinitionSnippet.test.ts @@ -29,8 +29,8 @@ describe('stepDefinitionSnippet', () => { content: snippet, } const result = expressionBuilder.build([source], []) - if (result.expressionLinks.length === 1) { - assert.strictEqual(result.expressionLinks[0].expression.source, '{int} is not {int}') + if (Array.from(result.expressionLinks.values()).flat().length === 1) { + assert.strictEqual(Array.from(result.expressionLinks.values()).flat()[0].expression.source, '{int} is not {int}') } console.log(`### Manually verify that this is valid ${languageName}:`) console.log(snippet) From 69086419ff72c6ca3a7f78d4a8b52816b09848bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Tue, 10 Feb 2026 17:08:03 +0200 Subject: [PATCH 5/9] incremental suggestions --- src/language/ExpressionBuilder.ts | 8 +++-- src/language/types.ts | 1 + src/messages/MessagesBuilder.ts | 2 +- src/suggestions/buildSuggestions.ts | 39 +++++++++++++++-------- test/suggestions/buildSuggestions.test.ts | 2 +- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index 8631770c..bb1815c1 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -66,6 +66,7 @@ export class ExpressionBuilder { }) return { + newExpressionLinks: new Map(), // during initial build keep empty expressionLinks, parameterTypeLinks, errors: sourceAnalyser.getErrors().concat(errors), @@ -81,17 +82,17 @@ export class ExpressionBuilder { const errors: Error[] = [] const registry = existingResult.registry const expressionFactory = new ExpressionFactory(registry) + const newExpressionLinks: Map = new Map() const sourceAnalyser = new SourceAnalyzer(this.parserAdapter, sources) // TODO: we cant currently override existing parameter type as it raises an error + // We could catch the error and ignore it // const parameterTypeLinks: ParameterTypeLink[] = [] // sourceAnalyser.eachParameterTypeLink((parameterTypeLink) => { // defineParameterType(parameterTypeLink.parameterType) // parameterTypeLinks.push(parameterTypeLink) // }) - - // extract to private method const cleared = new Map() sourceAnalyser.eachStepDefinitionExpression( @@ -103,9 +104,11 @@ export class ExpressionBuilder { // clear the existing expression links for this source if (!cleared.get(source.uri)) { existingResult.expressionLinks.set(source.uri, []) + newExpressionLinks.set(source.uri, []) cleared.set(source.uri, true) } existingResult.expressionLinks.get(source.uri)?.push({ expression, locationLink }) + newExpressionLinks.get(source.uri)?.push({ expression, locationLink }) } catch (err) { errors.push(err) } @@ -113,6 +116,7 @@ export class ExpressionBuilder { ) return { + newExpressionLinks, expressionLinks: existingResult.expressionLinks, parameterTypeLinks: existingResult.parameterTypeLinks, errors: sourceAnalyser.getErrors().concat(errors), diff --git a/src/language/types.ts b/src/language/types.ts index dbcea07a..5c7668d7 100644 --- a/src/language/types.ts +++ b/src/language/types.ts @@ -68,6 +68,7 @@ export type Language = Readonly<{ }> export type ExpressionBuilderResult = Readonly<{ + readonly newExpressionLinks: Map readonly expressionLinks: Map readonly parameterTypeLinks: Map readonly errors: Error[] diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index 6f5be52b..4178e62a 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -64,7 +64,7 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - suggestions: buildSuggestions(this.parameterTypeRegistry, new Set(this.stepTexts), this.expressions), + suggestions: Array.from(buildSuggestions(this.parameterTypeRegistry, new Set(this.stepTexts), this.expressions, new Map(), false).values()), expressions: this.expressions, } } diff --git a/src/suggestions/buildSuggestions.ts b/src/suggestions/buildSuggestions.ts index 1e75c7fe..dc8b06dd 100644 --- a/src/suggestions/buildSuggestions.ts +++ b/src/suggestions/buildSuggestions.ts @@ -22,10 +22,10 @@ export function buildSuggestions( registry: ParameterTypeRegistry, stepTexts: Set, expressions: readonly Expression[], + suggestions: Map, + addUnmatchedStepTexts = true, maxChoices = 10 -): readonly Suggestion[] { - let suggestions: Suggestion[] = [] - +): Map { const parameterChoiceSets: Record> = {} const unmatchedStepTexts = new Set(stepTexts) for (const expression of expressions) { @@ -57,23 +57,34 @@ export function buildSuggestions( for (const expression of expressions) { if (expression instanceof CucumberExpression) { - suggestions = suggestions.concat( - buildSuggestionFromCucumberExpression(expression, registry, parameterChoices) + const suggestion = buildSuggestionFromCucumberExpression( + expression, + registry, + parameterChoices ) + suggestions.set(suggestion.label, suggestion) } if (expression instanceof RegularExpression) { - suggestions = suggestions.concat( - buildSuggestionsFromRegularExpression(expression, registry, stepTexts, parameterChoices) + const suggests = buildSuggestionsFromRegularExpression( + expression, + registry, + stepTexts, + parameterChoices ) + for (const suggestion of suggests) { + suggestions.set(suggestion.label, suggestion) + } } } - for (const stepText of unmatchedStepTexts) { - suggestions.push({ - label: stepText, - segments: [stepText], - matched: false, - }) + if (addUnmatchedStepTexts) { + for (const stepText of unmatchedStepTexts) { + suggestions.set(stepText, { + label: stepText, + segments: [stepText], + matched: false, + }) + } } - return suggestions.sort((a, b) => a.label.localeCompare(b.label)) + return suggestions } diff --git a/test/suggestions/buildSuggestions.test.ts b/test/suggestions/buildSuggestions.test.ts index 2a67daee..56cf7c8b 100644 --- a/test/suggestions/buildSuggestions.test.ts +++ b/test/suggestions/buildSuggestions.test.ts @@ -162,6 +162,6 @@ function assertSuggestions( expectedSuggestions: Suggestion[], maxChoices = 10 ) { - const suggestions = buildSuggestions(parameterTypeRegistry, stepTexts, expressions, maxChoices) + const suggestions = buildSuggestions(parameterTypeRegistry, stepTexts, expressions, new Map(), true, maxChoices) assert.deepStrictEqual(suggestions, expectedSuggestions) } From b9c907b718169c5f5bd8fc492b1f986845861566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Tue, 24 Feb 2026 12:18:06 +0200 Subject: [PATCH 6/9] fix eslint and tests --- .eslintrc.json | 1 + .mocharc.cjs | 2 +- .mocharc.cjs.cjs | 2 +- src/language/ExpressionBuilder.ts | 12 ++++---- src/language/SourceAnalyzer.ts | 4 ++- src/messages/MessagesBuilder.ts | 10 ++++++- test/language/ExpressionBuilder.test.ts | 28 +++++++++++-------- .../snippet/stepDefinitionSnippet.test.ts | 5 +++- test/suggestions/buildSuggestions.test.ts | 11 +++++++- ...ldSuggestionsFromRegularExpression.test.ts | 11 ++++++-- 10 files changed, 59 insertions(+), 27 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 6d158a2c..5d2af9c8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "root": true, "env": { "browser": true, "node": true diff --git a/.mocharc.cjs b/.mocharc.cjs index 714b80bc..33e64911 100644 --- a/.mocharc.cjs +++ b/.mocharc.cjs @@ -1,6 +1,6 @@ const fs = require('fs') const ignore = ['test/language/testdata/**/*'] -if (!fs.existsSync('node_modules/tree-sitter-java')) { +if (!fs.existsSync('node_modules/tree-sitter')) { ignore.push('test/language/ExpressionBuilder.test.*') ignore.push('test/service/snippet/stepDefinitionSnippet.test.*') } diff --git a/.mocharc.cjs.cjs b/.mocharc.cjs.cjs index 81bcb282..7ffd1fcd 100644 --- a/.mocharc.cjs.cjs +++ b/.mocharc.cjs.cjs @@ -1,6 +1,6 @@ const fs = require('fs') const ignore = ['dist/cjs/test/language/testdata/**/*'] -if (!fs.existsSync('node_modules/tree-sitter-java')) { +if (!fs.existsSync('node_modules/tree-sitter')) { ignore.push('dist/cjs/test/language/ExpressionBuilder.test.*') ignore.push('dist/cjs/test/service/snippet/stepDefinitionSnippet.test.*') } diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index bb1815c1..ec233172 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -4,7 +4,7 @@ import { ParameterTypeRegistry, } from '@cucumber/cucumber-expressions' -import { createLocationLink, makeParameterType, sortLinks } from './helpers.js' +import { createLocationLink, makeParameterType } from './helpers.js' import { SourceAnalyzer } from './SourceAnalyzer.js' import { ExpressionBuilderResult, @@ -60,10 +60,11 @@ export class ExpressionBuilder { if (!expressionLinks.get(source.uri)) { expressionLinks.set(source.uri, [{ expression, locationLink }]) } - } catch (err) { - errors.push(err) + } catch (err) { + errors.push(err) + } } - }) + ) return { newExpressionLinks: new Map(), // during initial build keep empty @@ -93,11 +94,10 @@ export class ExpressionBuilder { // defineParameterType(parameterTypeLink.parameterType) // parameterTypeLinks.push(parameterTypeLink) // }) - + const cleared = new Map() sourceAnalyser.eachStepDefinitionExpression( (stepDefinitionExpression, rootNode, expressionNode, source) => { - try { const expression = expressionFactory.createExpression(stepDefinitionExpression) const locationLink = createLocationLink(rootNode, expressionNode, source.uri) diff --git a/src/language/SourceAnalyzer.ts b/src/language/SourceAnalyzer.ts index 845bbb0d..e8fb3ef6 100644 --- a/src/language/SourceAnalyzer.ts +++ b/src/language/SourceAnalyzer.ts @@ -31,7 +31,9 @@ export class SourceAnalyzer { private readonly sources: readonly Source[] ) {} - eachParameterTypeLink(callback: (parameterTypeLink: ParameterTypeLink, source: Source) => void) { + eachParameterTypeLink( + callback: (parameterTypeLink: ParameterTypeLink, source: Source) => void + ) { const parameterTypeMatches = this.getSourceMatches( (language: Language) => language.defineParameterTypeQueries ) diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index 4178e62a..90e32f9a 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -64,7 +64,15 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - suggestions: Array.from(buildSuggestions(this.parameterTypeRegistry, new Set(this.stepTexts), this.expressions, new Map(), false).values()), + suggestions: Array.from( + buildSuggestions( + this.parameterTypeRegistry, + new Set(this.stepTexts), + this.expressions, + new Map(), + false + ).values() + ).sort((a, b) => a.label.localeCompare(b.label)), expressions: this.expressions, } } diff --git a/test/language/ExpressionBuilder.test.ts b/test/language/ExpressionBuilder.test.ts index a8483eb3..15076d9b 100644 --- a/test/language/ExpressionBuilder.test.ts +++ b/test/language/ExpressionBuilder.test.ts @@ -37,9 +37,9 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = Array.from(result.parameterTypeLinks.values()).flat().find( - (l) => l.parameterType.name === 'WithoutExpression' - )?.parameterType?.regexpStrings + const regexpStrings = Array.from(result.parameterTypeLinks.values()) + .flat() + .find((l) => l.parameterType.name === 'WithoutExpression')?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['.*']) }) @@ -47,9 +47,9 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = Array.from(result.parameterTypeLinks.values()).flat().find( - (l) => l.parameterType.name === 'DateTime' - )?.parameterType?.regexpStrings + const regexpStrings = Array.from(result.parameterTypeLinks.values()) + .flat() + .find((l) => l.parameterType.name === 'DateTime')?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['today', 'tomorrow', '(.*) days later']) }) } @@ -65,7 +65,9 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { ]) // verify that the targetSelectionRange is inside the targetRange - for (const link of Array.from(result.expressionLinks.values()).flat().map((l) => l.locationLink)) { + for (const link of Array.from(result.expressionLinks.values()) + .flat() + .map((l) => l.locationLink)) { assert( link.targetSelectionRange.start.line > link.targetRange.start.line || link.targetSelectionRange.start.character >= link.targetRange.start.character @@ -75,11 +77,13 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { link.targetSelectionRange.end.character <= link.targetRange.end.character ) } - const expressions = Array.from(result.expressionLinks.values()).flat().map(({ expression }) => - expression instanceof CucumberExpression - ? expression.source - : (expression as RegularExpression).regexp - ) + const expressions = Array.from(result.expressionLinks.values()) + .flat() + .map(({ expression }) => + expression instanceof CucumberExpression + ? expression.source + : (expression as RegularExpression).regexp + ) const errors = result.errors.map((e) => e.message) if (cucumberExpressionsSupport.has(languageName)) { assert.deepStrictEqual(expressions, [ diff --git a/test/service/snippet/stepDefinitionSnippet.test.ts b/test/service/snippet/stepDefinitionSnippet.test.ts index fe221ed4..5ca320c7 100644 --- a/test/service/snippet/stepDefinitionSnippet.test.ts +++ b/test/service/snippet/stepDefinitionSnippet.test.ts @@ -30,7 +30,10 @@ describe('stepDefinitionSnippet', () => { } const result = expressionBuilder.build([source], []) if (Array.from(result.expressionLinks.values()).flat().length === 1) { - assert.strictEqual(Array.from(result.expressionLinks.values()).flat()[0].expression.source, '{int} is not {int}') + assert.strictEqual( + Array.from(result.expressionLinks.values()).flat()[0].expression.source, + '{int} is not {int}' + ) } console.log(`### Manually verify that this is valid ${languageName}:`) console.log(snippet) diff --git a/test/suggestions/buildSuggestions.test.ts b/test/suggestions/buildSuggestions.test.ts index 56cf7c8b..46ba0a94 100644 --- a/test/suggestions/buildSuggestions.test.ts +++ b/test/suggestions/buildSuggestions.test.ts @@ -162,6 +162,15 @@ function assertSuggestions( expectedSuggestions: Suggestion[], maxChoices = 10 ) { - const suggestions = buildSuggestions(parameterTypeRegistry, stepTexts, expressions, new Map(), true, maxChoices) + const suggestions = Array.from( + buildSuggestions( + parameterTypeRegistry, + stepTexts, + expressions, + new Map(), + true, + maxChoices + ).values() + ).sort((a, b) => a.label.localeCompare(b.label)) assert.deepStrictEqual(suggestions, expectedSuggestions) } diff --git a/test/suggestions/buildSuggestionsFromRegularExpression.test.ts b/test/suggestions/buildSuggestionsFromRegularExpression.test.ts index 2969e4bf..43fd5dee 100644 --- a/test/suggestions/buildSuggestionsFromRegularExpression.test.ts +++ b/test/suggestions/buildSuggestionsFromRegularExpression.test.ts @@ -33,9 +33,14 @@ describe('buildSuggestionsFromRegularExpression', () => { label: 'I have (\\d+) cukes', matched: true, } - const actual = buildSuggestionsFromRegularExpression(expression, registry, new Set(['I have 4 cukes']), { - '-?\\d+|\\d+': ['12'], - }) + const actual = buildSuggestionsFromRegularExpression( + expression, + registry, + new Set(['I have 4 cukes']), + { + '-?\\d+|\\d+': ['12'], + } + ) assert.deepStrictEqual(actual, [expected]) }) From d459f8f866c5cf188ec354ba2526a7f78965c049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Tue, 24 Feb 2026 17:19:13 +0200 Subject: [PATCH 7/9] extract utility method to reuse --- package.json | 4 +- src/language/ExpressionBuilder.ts | 48 +++++++++++++------ src/messages/MessagesBuilder.ts | 8 ++-- src/suggestions/buildSuggestions.ts | 6 +++ test/language/ExpressionBuilder.test.ts | 18 ++++--- .../snippet/stepDefinitionSnippet.test.ts | 4 +- 6 files changed, 56 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 3e2a9325..b722c7b8 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "module": "dist/esm/src/index.js", "types": "dist/esm/src/index.d.ts", "engines": { - "node": "18 || 20 || 22 || >=23" + "node": "18 || 20 || 22 || >=23 || >=24" }, "enginesTested": { - "node": "18 || 20 || 22 || 23" + "node": "18 || 20 || 22 || 23 || 24" }, "exports": { ".": { diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index ec233172..9b9eaad0 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -44,10 +44,10 @@ export class ExpressionBuilder { const parameterTypeLinks: Map = new Map() sourceAnalyser.eachParameterTypeLink((parameterTypeLink, source) => { defineParameterType(parameterTypeLink.parameterType) - parameterTypeLinks.get(source.uri)?.push(parameterTypeLink) - if (!parameterTypeLinks.get(source.uri)) { - parameterTypeLinks.set(source.uri, [parameterTypeLink]) + if (!parameterTypeLinks.has(source.uri)) { + parameterTypeLinks.set(source.uri, []) } + parameterTypeLinks.get(source.uri)?.push(parameterTypeLink) }) const expressionLinks: Map = new Map() @@ -56,10 +56,10 @@ export class ExpressionBuilder { try { const expression = expressionFactory.createExpression(stepDefinitionExpression) const locationLink = createLocationLink(rootNode, expressionNode, source.uri) - expressionLinks.get(source.uri)?.push({ expression, locationLink }) - if (!expressionLinks.get(source.uri)) { - expressionLinks.set(source.uri, [{ expression, locationLink }]) + if (!expressionLinks.has(source.uri)) { + expressionLinks.set(source.uri, []) } + expressionLinks.get(source.uri)?.push({ expression, locationLink }) } catch (err) { errors.push(err) } @@ -75,7 +75,12 @@ export class ExpressionBuilder { } } - // update existing result with new sources + /** + * Rebuilds the expression builder result with new sources + * @param existingResult - The existing expression builder result + * @param sources - The new sources to update the expression builder result with + * @returns The updated expression builder result + */ rebuild( existingResult: ExpressionBuilderResult, sources: readonly Source[] @@ -87,13 +92,20 @@ export class ExpressionBuilder { const sourceAnalyser = new SourceAnalyzer(this.parserAdapter, sources) - // TODO: we cant currently override existing parameter type as it raises an error - // We could catch the error and ignore it - // const parameterTypeLinks: ParameterTypeLink[] = [] - // sourceAnalyser.eachParameterTypeLink((parameterTypeLink) => { - // defineParameterType(parameterTypeLink.parameterType) - // parameterTypeLinks.push(parameterTypeLink) - // }) + // redefine is not allowed, so we ignore the error + function defineParameterType(parameterType: ParameterType) { + try { + registry.defineParameterType(parameterType) + } catch (err) { + // ignore error + } + } + + const parameterTypeLinks: ParameterTypeLink[] = [] + sourceAnalyser.eachParameterTypeLink((parameterTypeLink) => { + defineParameterType(parameterTypeLink.parameterType) + parameterTypeLinks.push(parameterTypeLink) + }) const cleared = new Map() sourceAnalyser.eachStepDefinitionExpression( @@ -123,4 +135,12 @@ export class ExpressionBuilder { registry, } } + + parameterTypeLinks(existingResult: ExpressionBuilderResult): ParameterTypeLink[] { + return Array.from(existingResult.parameterTypeLinks.values()).flat() + } + + expressionLinks(existingResult: ExpressionBuilderResult): ExpressionLink[] { + return Array.from(existingResult.expressionLinks.values()).flat() + } } diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index 90e32f9a..b203cef0 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -7,7 +7,7 @@ import { import { Envelope, StepDefinitionPatternType } from '@cucumber/messages' import { extractStepTexts } from '../gherkin/extractStepTexts.js' -import { buildSuggestions } from '../suggestions/buildSuggestions.js' +import { buildSuggestions, sortedSuggestions } from '../suggestions/buildSuggestions.js' import { Suggestion } from '../suggestions/types.js' export type MessagesBuilderResult = { @@ -64,15 +64,15 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - suggestions: Array.from( + suggestions: sortedSuggestions( buildSuggestions( this.parameterTypeRegistry, new Set(this.stepTexts), this.expressions, new Map(), false - ).values() - ).sort((a, b) => a.label.localeCompare(b.label)), + ) + ), expressions: this.expressions, } } diff --git a/src/suggestions/buildSuggestions.ts b/src/suggestions/buildSuggestions.ts index dc8b06dd..1c7a7fdf 100644 --- a/src/suggestions/buildSuggestions.ts +++ b/src/suggestions/buildSuggestions.ts @@ -88,3 +88,9 @@ export function buildSuggestions( } return suggestions } + +export function sortedSuggestions(suggestions: Map): Suggestion[] { + return Array.from(suggestions.values()).sort((a, b) => { + return a.label.localeCompare(b.label) + }) +} diff --git a/test/language/ExpressionBuilder.test.ts b/test/language/ExpressionBuilder.test.ts index 15076d9b..de98709c 100644 --- a/test/language/ExpressionBuilder.test.ts +++ b/test/language/ExpressionBuilder.test.ts @@ -37,8 +37,8 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = Array.from(result.parameterTypeLinks.values()) - .flat() + const regexpStrings = expressionBuilder + .parameterTypeLinks(result) .find((l) => l.parameterType.name === 'WithoutExpression')?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['.*']) }) @@ -47,8 +47,8 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = Array.from(result.parameterTypeLinks.values()) - .flat() + const regexpStrings = expressionBuilder + .parameterTypeLinks(result) .find((l) => l.parameterType.name === 'DateTime')?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['today', 'tomorrow', '(.*) days later']) }) @@ -65,9 +65,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { ]) // verify that the targetSelectionRange is inside the targetRange - for (const link of Array.from(result.expressionLinks.values()) - .flat() - .map((l) => l.locationLink)) { + for (const link of expressionBuilder.expressionLinks(result).map((l) => l.locationLink)) { assert( link.targetSelectionRange.start.line > link.targetRange.start.line || link.targetSelectionRange.start.character >= link.targetRange.start.character @@ -77,8 +75,8 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { link.targetSelectionRange.end.character <= link.targetRange.end.character ) } - const expressions = Array.from(result.expressionLinks.values()) - .flat() + const expressions = expressionBuilder + .expressionLinks(result) .map(({ expression }) => expression instanceof CucumberExpression ? expression.source @@ -106,7 +104,7 @@ Please register a ParameterType for 'undefined-parameter'`, // Verify that the extracted expressions actually work let matched = false - for (const expressionLink of Array.from(result.expressionLinks.values()).flat()) { + for (const expressionLink of expressionBuilder.expressionLinks(result)) { const match = expressionLink.expression.match('a 2020-12-24') if (match) { assert.strictEqual(match[0].getValue(undefined), '2020-12-24') diff --git a/test/service/snippet/stepDefinitionSnippet.test.ts b/test/service/snippet/stepDefinitionSnippet.test.ts index 5ca320c7..c3308781 100644 --- a/test/service/snippet/stepDefinitionSnippet.test.ts +++ b/test/service/snippet/stepDefinitionSnippet.test.ts @@ -29,9 +29,9 @@ describe('stepDefinitionSnippet', () => { content: snippet, } const result = expressionBuilder.build([source], []) - if (Array.from(result.expressionLinks.values()).flat().length === 1) { + if (expressionBuilder.expressionLinks(result).length === 1) { assert.strictEqual( - Array.from(result.expressionLinks.values()).flat()[0].expression.source, + expressionBuilder.expressionLinks(result)[0].expression.source, '{int} is not {int}' ) } From b49a3e915b3fd4fbfe26a187fdc91cda7b0f1026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Tue, 24 Feb 2026 17:34:43 +0200 Subject: [PATCH 8/9] putback sortlinks --- src/language/ExpressionBuilder.ts | 6 +++--- src/messages/MessagesBuilder.ts | 4 ++-- src/suggestions/buildSuggestions.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index 9b9eaad0..0af8b972 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -4,7 +4,7 @@ import { ParameterTypeRegistry, } from '@cucumber/cucumber-expressions' -import { createLocationLink, makeParameterType } from './helpers.js' +import { createLocationLink, makeParameterType, sortLinks } from './helpers.js' import { SourceAnalyzer } from './SourceAnalyzer.js' import { ExpressionBuilderResult, @@ -137,10 +137,10 @@ export class ExpressionBuilder { } parameterTypeLinks(existingResult: ExpressionBuilderResult): ParameterTypeLink[] { - return Array.from(existingResult.parameterTypeLinks.values()).flat() + return sortLinks(Array.from(existingResult.parameterTypeLinks.values()).flat()) } expressionLinks(existingResult: ExpressionBuilderResult): ExpressionLink[] { - return Array.from(existingResult.expressionLinks.values()).flat() + return sortLinks(Array.from(existingResult.expressionLinks.values()).flat()) } } diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index b203cef0..f2cef071 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -7,7 +7,7 @@ import { import { Envelope, StepDefinitionPatternType } from '@cucumber/messages' import { extractStepTexts } from '../gherkin/extractStepTexts.js' -import { buildSuggestions, sortedSuggestions } from '../suggestions/buildSuggestions.js' +import { buildSuggestions, sortSuggestions } from '../suggestions/buildSuggestions.js' import { Suggestion } from '../suggestions/types.js' export type MessagesBuilderResult = { @@ -64,7 +64,7 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - suggestions: sortedSuggestions( + suggestions: sortSuggestions( buildSuggestions( this.parameterTypeRegistry, new Set(this.stepTexts), diff --git a/src/suggestions/buildSuggestions.ts b/src/suggestions/buildSuggestions.ts index 1c7a7fdf..1398e892 100644 --- a/src/suggestions/buildSuggestions.ts +++ b/src/suggestions/buildSuggestions.ts @@ -89,7 +89,7 @@ export function buildSuggestions( return suggestions } -export function sortedSuggestions(suggestions: Map): Suggestion[] { +export function sortSuggestions(suggestions: Map): Suggestion[] { return Array.from(suggestions.values()).sort((a, b) => { return a.label.localeCompare(b.label) }) From c49b18c40e774037f17f2c9e960fff7f6bcf09e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Sergijevskis?= Date: Wed, 25 Feb 2026 13:11:01 +0200 Subject: [PATCH 9/9] move parameterTypeLinks to helpers --- src/language/ExpressionBuilder.ts | 10 +------ src/language/helpers.ts | 18 +++++++++++- src/language/index.ts | 1 + test/language/ExpressionBuilder.test.ts | 29 +++++++++---------- .../snippet/stepDefinitionSnippet.test.ts | 8 ++--- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/language/ExpressionBuilder.ts b/src/language/ExpressionBuilder.ts index 0af8b972..cb145412 100644 --- a/src/language/ExpressionBuilder.ts +++ b/src/language/ExpressionBuilder.ts @@ -4,7 +4,7 @@ import { ParameterTypeRegistry, } from '@cucumber/cucumber-expressions' -import { createLocationLink, makeParameterType, sortLinks } from './helpers.js' +import { createLocationLink, makeParameterType } from './helpers.js' import { SourceAnalyzer } from './SourceAnalyzer.js' import { ExpressionBuilderResult, @@ -135,12 +135,4 @@ export class ExpressionBuilder { registry, } } - - parameterTypeLinks(existingResult: ExpressionBuilderResult): ParameterTypeLink[] { - return sortLinks(Array.from(existingResult.parameterTypeLinks.values()).flat()) - } - - expressionLinks(existingResult: ExpressionBuilderResult): ExpressionLink[] { - return sortLinks(Array.from(existingResult.expressionLinks.values()).flat()) - } } diff --git a/src/language/helpers.ts b/src/language/helpers.ts index 9c397f0c..b01d8434 100644 --- a/src/language/helpers.ts +++ b/src/language/helpers.ts @@ -1,7 +1,15 @@ import { ParameterType, RegExps } from '@cucumber/cucumber-expressions' import { DocumentUri, LocationLink, Range } from 'vscode-languageserver-types' -import { Link, NodePredicate, TreeSitterQueryMatch, TreeSitterSyntaxNode } from './types' +import { + ExpressionBuilderResult, + ExpressionLink, + Link, + NodePredicate, + ParameterTypeLink, + TreeSitterQueryMatch, + TreeSitterSyntaxNode, +} from './types' export function syntaxNode(match: TreeSitterQueryMatch, name: string): TreeSitterSyntaxNode | null { const nodes = syntaxNodes(match, name) @@ -26,6 +34,14 @@ export function sortLinks(links: L[]): L[] { }) } +export function parameterTypeLinks(existingResult: ExpressionBuilderResult): ParameterTypeLink[] { + return sortLinks(Array.from(existingResult.parameterTypeLinks.values()).flat()) +} + +export function expressionLinks(existingResult: ExpressionBuilderResult): ExpressionLink[] { + return sortLinks(Array.from(existingResult.expressionLinks.values()).flat()) +} + export function createLocationLink( rootNode: TreeSitterSyntaxNode, selectionNode: TreeSitterSyntaxNode, diff --git a/src/language/index.ts b/src/language/index.ts index 07898489..ab056edc 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -1,2 +1,3 @@ export * from './ExpressionBuilder.js' +export { expressionLinks, parameterTypeLinks } from './helpers.js' export * from './types.js' diff --git a/test/language/ExpressionBuilder.test.ts b/test/language/ExpressionBuilder.test.ts index de98709c..51dc3f45 100644 --- a/test/language/ExpressionBuilder.test.ts +++ b/test/language/ExpressionBuilder.test.ts @@ -3,6 +3,7 @@ import assert from 'assert' import { readFile } from 'fs/promises' import { glob } from 'glob' import { basename, resolve } from 'path' +import { expressionLinks, parameterTypeLinks } from 'src/language/helpers.js' import { ExpressionBuilder, LanguageName } from '../../src/index.js' import { ParserAdapter, Source } from '../../src/language/types.js' @@ -37,9 +38,9 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = expressionBuilder - .parameterTypeLinks(result) - .find((l) => l.parameterType.name === 'WithoutExpression')?.parameterType?.regexpStrings + const regexpStrings = parameterTypeLinks(result).find( + (l) => l.parameterType.name === 'WithoutExpression' + )?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['.*']) }) @@ -47,9 +48,9 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { const sources = await loadSources(dir, languageName) const result = expressionBuilder.build(sources, []) - const regexpStrings = expressionBuilder - .parameterTypeLinks(result) - .find((l) => l.parameterType.name === 'DateTime')?.parameterType?.regexpStrings + const regexpStrings = parameterTypeLinks(result).find( + (l) => l.parameterType.name === 'DateTime' + )?.parameterType?.regexpStrings assert.deepStrictEqual(regexpStrings, ['today', 'tomorrow', '(.*) days later']) }) } @@ -65,7 +66,7 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { ]) // verify that the targetSelectionRange is inside the targetRange - for (const link of expressionBuilder.expressionLinks(result).map((l) => l.locationLink)) { + for (const link of expressionLinks(result).map((l) => l.locationLink)) { assert( link.targetSelectionRange.start.line > link.targetRange.start.line || link.targetSelectionRange.start.character >= link.targetRange.start.character @@ -75,13 +76,11 @@ function defineContract(makeParserAdapter: () => ParserAdapter) { link.targetSelectionRange.end.character <= link.targetRange.end.character ) } - const expressions = expressionBuilder - .expressionLinks(result) - .map(({ expression }) => - expression instanceof CucumberExpression - ? expression.source - : (expression as RegularExpression).regexp - ) + const expressions = expressionLinks(result).map(({ expression }) => + expression instanceof CucumberExpression + ? expression.source + : (expression as RegularExpression).regexp + ) const errors = result.errors.map((e) => e.message) if (cucumberExpressionsSupport.has(languageName)) { assert.deepStrictEqual(expressions, [ @@ -104,7 +103,7 @@ Please register a ParameterType for 'undefined-parameter'`, // Verify that the extracted expressions actually work let matched = false - for (const expressionLink of expressionBuilder.expressionLinks(result)) { + for (const expressionLink of expressionLinks(result)) { const match = expressionLink.expression.match('a 2020-12-24') if (match) { assert.strictEqual(match[0].getValue(undefined), '2020-12-24') diff --git a/test/service/snippet/stepDefinitionSnippet.test.ts b/test/service/snippet/stepDefinitionSnippet.test.ts index c3308781..dd54cac6 100644 --- a/test/service/snippet/stepDefinitionSnippet.test.ts +++ b/test/service/snippet/stepDefinitionSnippet.test.ts @@ -1,5 +1,6 @@ import { CucumberExpressionGenerator, ParameterTypeRegistry } from '@cucumber/cucumber-expressions' import assert from 'assert' +import { expressionLinks } from 'src/language/helpers.js' import { ExpressionBuilder } from '../../../src/language/ExpressionBuilder.js' import { getLanguage } from '../../../src/language/languages.js' @@ -29,11 +30,8 @@ describe('stepDefinitionSnippet', () => { content: snippet, } const result = expressionBuilder.build([source], []) - if (expressionBuilder.expressionLinks(result).length === 1) { - assert.strictEqual( - expressionBuilder.expressionLinks(result)[0].expression.source, - '{int} is not {int}' - ) + if (expressionLinks(result).length === 1) { + assert.strictEqual(expressionLinks(result)[0].expression.source, '{int} is not {int}') } console.log(`### Manually verify that this is valid ${languageName}:`) console.log(snippet)