diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 92717759..90133816 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -121,6 +121,7 @@ describe('Language Support', () => { expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); + expect(languages).toContain('groovy'); }); }); @@ -3895,3 +3896,123 @@ 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'); + }); +}); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index c78c52ce..24828411 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', }; /** @@ -92,6 +93,11 @@ export const EXTENSION_MAP: Record = { '.sc': 'scala', '.lua': 'lua', '.luau': 'luau', + '.groovy': 'groovy', + '.gradle': 'groovy', + '.gvy': 'groovy', + '.gy': 'groovy', + '.gsh': 'groovy', }; /** @@ -155,7 +161,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 a289f028..e72bf2df 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'; export const EXTRACTORS: Partial> = { typescript: typescriptExtractor, @@ -47,4 +48,5 @@ export const EXTRACTORS: Partial> = { scala: scalaExtractor, lua: luaExtractor, luau: luauExtractor, + groovy: groovyExtractor, }; diff --git a/src/extraction/wasm/tree-sitter-groovy.wasm b/src/extraction/wasm/tree-sitter-groovy.wasm new file mode 100644 index 00000000..46db63d5 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 0168665d..b952f99f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,7 @@ export const LANGUAGES = [ 'scala', 'lua', 'luau', + 'groovy', 'yaml', 'twig', 'unknown',