From d82ef1a1ac282681fd26a83b62a943dfe9faaa1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Dan=C3=B3czy?= Date: Tue, 23 Jun 2026 23:36:38 +0200 Subject: [PATCH] feat: add Kotlin step definition support - Add kotlinLanguage with tree-sitter queries for class-based step definitions (@Given/@When/@Then) and @ParameterType annotations, including expression-body functions (= expr syntax) - Add 'kotlin' to LanguageNames and language registry - Add Kotlin case to NodeParserAdapter - Add tree-sitter-kotlin to optionalDependencies - Add kotlin to scripts/build.js so kotlin.wasm is built by prepare Co-Authored-By: Claude Sonnet 4.6 (1M context) --- package.json | 3 +- scripts/build.js | 5 ++ src/language/kotlinLanguage.ts | 88 +++++++++++++++++++++++ src/language/languages.ts | 2 + src/language/types.ts | 1 + src/tree-sitter-node/NodeParserAdapter.ts | 5 ++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/language/kotlinLanguage.ts diff --git a/package.json b/package.json index 8bec8b3b..07370aa8 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "tree-sitter-ruby": "0.23.1", "tree-sitter-rust": "0.23.1", "tree-sitter-scala": "^0.23.4", - "tree-sitter-typescript": "0.23.2" + "tree-sitter-typescript": "0.23.2", + "tree-sitter-kotlin": "^0.3.8" }, "devDependencies": { "@cucumber/cucumber": "^13.0.0", diff --git a/scripts/build.js b/scripts/build.js index 5a3c7be0..c247fa6a 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -50,6 +50,11 @@ const languages = [ dir: '', wasm: 'scala', }, + { + npm: 'tree-sitter-kotlin', + dir: '', + wasm: 'kotlin', + }, ] // Build wasm parsers for supported languages diff --git a/src/language/kotlinLanguage.ts b/src/language/kotlinLanguage.ts new file mode 100644 index 00000000..df93a0d2 --- /dev/null +++ b/src/language/kotlinLanguage.ts @@ -0,0 +1,88 @@ +import { Language, TreeSitterSyntaxNode } from './types.js' + +export const kotlinLanguage: Language = { + toParameterTypeName(node) { + switch (node.type) { + case 'string_literal': { + return stringLiteral(node) + } + case 'simple_identifier': { + return node.text + } + default: { + throw new Error(`Unsupported node type ${node.type}`) + } + } + }, + toParameterTypeRegExps(node) { + return stringLiteral(node) + }, + toStepDefinitionExpression(node) { + if (node.type === 'string_literal') { + const text = stringLiteral(node) + const hasRegExpAnchors = text[0] == '^' || text[text.length - 1] == '$' + return hasRegExpAnchors ? new RegExp(text) : text + } + throw new Error(`Unsupported node type ${node.type}`) + }, + + defineParameterTypeQueries: [ + ` +(function_declaration + (modifiers + (annotation + (constructor_invocation + (user_type (type_identifier) @annotation-name) + (value_arguments (value_argument (string_literal) @expression)) + ) + ) + ) + (simple_identifier) @name + (#eq? @annotation-name "ParameterType") +) @root +`, + ], + defineStepDefinitionQueries: [ + ` +(function_declaration + (modifiers + (annotation + (constructor_invocation + (user_type (type_identifier) @annotation-name) + (value_arguments (value_argument (string_literal) @expression)) + ) + ) + ) + (#match? @annotation-name "Given|When|Then|And|But") +) @root +`, + ], + snippetParameters: { + int: { type: 'Int', name: 'i' }, + float: { type: 'Float', name: 'f' }, + word: { type: 'String' }, + string: { type: 'String', name: 's' }, + double: { type: 'Double', name: 'd' }, + bigdecimal: { type: 'java.math.BigDecimal', name: 'bigDecimal' }, + byte: { type: 'Byte', name: 'b' }, + short: { type: 'Short', name: 's' }, + long: { type: 'Long', name: 'l' }, + biginteger: { type: 'java.math.BigInteger', name: 'bigInteger' }, + '': { type: 'Any', name: 'arg' }, + }, + defaultSnippetTemplate: ` + @{{ keyword }}("{{ expression }}") + fun {{ #underscore }}{{ expression }}{{ /underscore }}({{ #parameters }}{{ #seenParameter }}, {{ /seenParameter }}{{ name }}: {{ type }}{{ /parameters }}) { + // {{ blurb }} + } +`, +} + +function stringLiteral(node: TreeSitterSyntaxNode | null): string { + if (node === null) throw new Error('node cannot be null') + const text = node.text + if (text.startsWith('"') && text.endsWith('"')) { + return text.slice(1, -1).replace(/\\\\/g, '\\') + } + return text +} diff --git a/src/language/languages.ts b/src/language/languages.ts index 7b325b1b..d2d11f8c 100644 --- a/src/language/languages.ts +++ b/src/language/languages.ts @@ -2,6 +2,7 @@ import { csharpLanguage } from './csharpLanguage.js' import { goLanguage } from './goLanguage.js' import { javaLanguage } from './javaLanguage.js' import { javascriptLanguage } from './javascriptLanguage.js' +import { kotlinLanguage } from './kotlinLanguage.js' import { phpLanguage } from './phpLanguage.js' import { pythonLanguage } from './pythonLanguage.js' import { rubyLanguage } from './rubyLanguage.js' @@ -21,6 +22,7 @@ const languageByName: Record = { javascript: javascriptLanguage, go: goLanguage, scala: scalaLanguage, + kotlin: kotlinLanguage, } export function getLanguage(languageName: LanguageName): Language { diff --git a/src/language/types.ts b/src/language/types.ts index 7929b75f..2fde0673 100644 --- a/src/language/types.ts +++ b/src/language/types.ts @@ -46,6 +46,7 @@ export const LanguageNames = [ 'javascript', 'go', 'scala', + 'kotlin', ] as const export type LanguageName = (typeof LanguageNames)[number] diff --git a/src/tree-sitter-node/NodeParserAdapter.ts b/src/tree-sitter-node/NodeParserAdapter.ts index c7c34aec..97f79f6c 100644 --- a/src/tree-sitter-node/NodeParserAdapter.ts +++ b/src/tree-sitter-node/NodeParserAdapter.ts @@ -7,6 +7,8 @@ import Go from 'tree-sitter-go' // @ts-ignore import Java from 'tree-sitter-java' // @ts-ignore +import Kotlin from 'tree-sitter-kotlin' +// @ts-ignore import Php from 'tree-sitter-php' // @ts-ignore import Python from 'tree-sitter-python' @@ -58,6 +60,9 @@ export class NodeParserAdapter implements ParserAdapter { case 'scala': this.parser.setLanguage(Scala) break + case 'kotlin': + this.parser.setLanguage(Kotlin) + break default: throw new Error(`Unsupported language: ${languageName}`) }