diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index d29fa11b3..074011127 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -129,6 +129,7 @@ describe('Language Support', () => { expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); + expect(languages).toContain('groovy'); }); }); @@ -4285,6 +4286,126 @@ local count = 0 }); }); +describe('Groovy Extraction', () => { + describe('Language detection', () => { + it('should detect Groovy files', () => { + expect(detectLanguage('build.gradle')).toBe('groovy'); + expect(detectLanguage('src/Main.groovy')).toBe('groovy'); + expect(detectLanguage('utils.gvy')).toBe('groovy'); + expect(detectLanguage('script.gy')).toBe('groovy'); + expect(detectLanguage('helper.gsh')).toBe('groovy'); + }); + }); + + it('should extract class declarations', () => { + const code = ` +class UserService { + private repository + + UserService(repository) { + this.repository = repository + } + + def getUser(String id) { + return repository.findById(id) + } +} +`; + const result = extractFromSource('UserService.groovy', code); + + const classNode = result.nodes.find((n) => n.kind === 'class'); + expect(classNode).toBeDefined(); + expect(classNode?.name).toBe('UserService'); + }); + + it('should extract method declarations', () => { + const code = ` +class Calculator { + static int add(int a, int b) { + return a + b + } +} +`; + const result = extractFromSource('Calculator.groovy', code); + + const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add'); + expect(methodNode).toBeDefined(); + expect(methodNode?.isStatic).toBe(true); + }); + + it('should extract def function declarations', () => { + const code = ` +def greet(String name) { + return "Hello, \${name}" +} +`; + const result = extractFromSource('utils.groovy', code); + + const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet'); + expect(funcNode).toBeDefined(); + expect(funcNode?.language).toBe('groovy'); + }); + + it('should extract interface declarations', () => { + const code = ` +interface Repository { + def findById(String id) + def save(Object entity) +} +`; + const result = extractFromSource('Repository.groovy', code); + + const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); + expect(ifaceNode).toBeDefined(); + expect(ifaceNode?.name).toBe('Repository'); + }); + + it('should extract enum declarations', () => { + const code = ` +enum Color { + RED, GREEN, BLUE +} +`; + const result = extractFromSource('Color.groovy', code); + + const enumNode = result.nodes.find((n) => n.kind === 'enum'); + expect(enumNode).toBeDefined(); + expect(enumNode?.name).toBe('Color'); + }); + + it('should extract imports', () => { + const code = ` +import java.util.List +import groovy.json.JsonSlurper +`; + const result = extractFromSource('App.groovy', code); + + const imports = result.nodes.filter((n) => n.kind === 'import'); + expect(imports.length).toBe(2); + expect(imports.map((n) => n.name)).toContain('java.util.List'); + expect(imports.map((n) => n.name)).toContain('groovy.json.JsonSlurper'); + }); + + it('should extract visibility modifiers', () => { + const code = ` +class Account { + private String secret + protected String internal + public String visible +} +`; + const result = extractFromSource('Account.groovy', code); + + const fields = result.nodes.filter((n) => n.kind === 'field'); + const secret = fields.find((n) => n.name === 'secret'); + const internal = fields.find((n) => n.name === 'internal'); + const visible = fields.find((n) => n.name === 'visible'); + expect(secret?.visibility).toBe('private'); + expect(internal?.visibility).toBe('protected'); + expect(visible?.visibility).toBe('public'); + }); +}); + // ============================================================================= // Objective-C // ============================================================================= @@ -4364,11 +4485,6 @@ void helperFunction(int count) { }); it('should reconstruct multi-keyword selectors at the call site so they resolve to the method definition', () => { - // Regression for the gap discovered post-#165: message_expression's - // multi-keyword form `[obj a:1 b:2]` was only emitting the first keyword, - // so calls never resolved to multi-part method definitions like - // `GET:parameters:headers:progress:success:failure:`. The call-site name - // must match the method-definition name with full keywords + trailing colons. const code = ` @implementation Caller - (void)demo { @@ -4452,7 +4568,6 @@ func (s Stack[T]) Len() int { return len(s.items) } expect(detectLanguage('mod.mts')).toBe('typescript'); expect(detectLanguage('service.xsjs')).toBe('javascript'); - // End-to-end: a .mts file is parsed as TS, a .xsjs file as JS. const ts = extractFromSource('mod.mts', 'export function hello(): number { return 1; }'); expect(ts.nodes.find((n) => n.name === 'hello' && n.kind === 'function')).toBeDefined(); const js = extractFromSource('service.xsjs', 'function handleRequest() { return 1; }'); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 576845e20..ccb4ece1e 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -37,6 +37,7 @@ const WASM_GRAMMAR_FILES: Record = { scala: 'tree-sitter-scala.wasm', lua: 'tree-sitter-lua.wasm', luau: 'tree-sitter-luau.wasm', + groovy: 'tree-sitter-groovy.wasm', objc: 'tree-sitter-objc.wasm', }; @@ -99,6 +100,11 @@ export const EXTENSION_MAP: Record = { '.sc': 'scala', '.lua': 'lua', '.luau': 'luau', + '.groovy': 'groovy', + '.gradle': 'groovy', + '.gvy': 'groovy', + '.gy': 'groovy', + '.gsh': 'groovy', '.m': 'objc', '.mm': 'objc', // XML: file-level tracking; the MyBatis extractor matches `` @@ -185,7 +191,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise { + const params = getChildByField(node, 'parameters'); + const returnType = getChildByField(node, 'type'); + if (!params) return undefined; + const paramsText = getNodeText(params, source); + return returnType ? getNodeText(returnType, source) + ' ' + paramsText : paramsText; + }, + getVisibility: (node) => { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child?.type === 'modifiers') { + const text = child.text; + if (text.includes('public')) return 'public'; + if (text.includes('private')) return 'private'; + if (text.includes('protected')) return 'protected'; + } + } + return undefined; + }, + isStatic: (node) => { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child?.type === 'modifiers' && child.text.includes('static')) { + return true; + } + } + return false; + }, + extractImport: (node, source) => { + const importText = source.substring(node.startIndex, node.endIndex).trim(); + const scopedId = node.namedChildren.find((c: SyntaxNode) => c.type === 'scoped_identifier'); + if (scopedId) { + const moduleName = source.substring(scopedId.startIndex, scopedId.endIndex); + return { moduleName, signature: importText }; + } + return null; + }, +}; diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 543598b8e..01d3c5782 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -25,6 +25,7 @@ import { pascalExtractor } from './pascal'; import { scalaExtractor } from './scala'; import { luaExtractor } from './lua'; import { luauExtractor } from './luau'; +import { groovyExtractor } from './groovy'; import { objcExtractor } from './objc'; export const EXTRACTORS: Partial> = { @@ -48,5 +49,6 @@ export const EXTRACTORS: Partial> = { scala: scalaExtractor, lua: luaExtractor, luau: luauExtractor, + groovy: groovyExtractor, objc: objcExtractor, }; diff --git a/src/extraction/wasm/tree-sitter-groovy.wasm b/src/extraction/wasm/tree-sitter-groovy.wasm new file mode 100644 index 000000000..46db63d5c Binary files /dev/null and b/src/extraction/wasm/tree-sitter-groovy.wasm differ diff --git a/src/types.ts b/src/types.ts index e710e31a1..ffe304774 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,7 @@ export const LANGUAGES = [ 'scala', 'lua', 'luau', + 'groovy', 'objc', 'yaml', 'twig',