Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe('Language Support', () => {
expect(languages).toContain('swift');
expect(languages).toContain('kotlin');
expect(languages).toContain('dart');
expect(languages).toContain('groovy');
});
});

Expand Down Expand Up @@ -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');
});
});
9 changes: 8 additions & 1 deletion src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
scala: 'tree-sitter-scala.wasm',
lua: 'tree-sitter-lua.wasm',
luau: 'tree-sitter-luau.wasm',
groovy: 'tree-sitter-groovy.wasm',
};

/**
Expand Down Expand Up @@ -92,6 +93,11 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.sc': 'scala',
'.lua': 'lua',
'.luau': 'luau',
'.groovy': 'groovy',
'.gradle': 'groovy',
'.gvy': 'groovy',
'.gy': 'groovy',
'.gsh': 'groovy',
};

/**
Expand Down Expand Up @@ -155,7 +161,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
// ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
// 0.25 (drops nested calls/imports on every file after the first); we
// vendor the upstream ABI-15 wasm instead.
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'groovy')
? path.join(__dirname, 'wasm', wasmFile)
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
const language = await WasmLanguage.load(wasmPath);
Expand Down Expand Up @@ -325,6 +331,7 @@ export function getLanguageDisplayName(language: Language): string {
scala: 'Scala',
lua: 'Lua',
luau: 'Luau',
groovy: 'Groovy',
yaml: 'YAML',
twig: 'Twig',
unknown: 'Unknown',
Expand Down
59 changes: 59 additions & 0 deletions src/extraction/languages/groovy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

export const groovyExtractor: LanguageExtractor = {
functionTypes: ['function_definition'],
classTypes: ['class_declaration'],
methodTypes: ['method_declaration', 'constructor_declaration'],
interfaceTypes: ['interface_declaration'],
structTypes: [],
enumTypes: ['enum_declaration'],
enumMemberTypes: ['enum_constant'],
typeAliasTypes: [],
importTypes: ['import_declaration'],
callTypes: ['method_invocation'],
variableTypes: ['local_variable_declaration'],
fieldTypes: ['field_declaration'],
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
returnField: 'type',
getSignature: (node, source) => {
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;
},
};
2 changes: 2 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand All @@ -47,4 +48,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
scala: scalaExtractor,
lua: luaExtractor,
luau: luauExtractor,
groovy: groovyExtractor,
};
Binary file added src/extraction/wasm/tree-sitter-groovy.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const LANGUAGES = [
'scala',
'lua',
'luau',
'groovy',
'yaml',
'twig',
'unknown',
Expand Down