Skip to content

Commit ae7ae0e

Browse files
committed
feat(julia): add Julia language support to CodeGraph
Add tree-sitter-julia extraction with vendored WASM and registry wiring. Builds on colbymchenry#244 (@kongdd) with support for common Julia 1.11 AST shapes: - getName hook and resolveBody for structs/functions without block wrappers - One-line assignment functions (e.g. f(x) = expr) - Module nodes and macrocall_expression call sites - Vendored tree-sitter-julia.wasm (not published in tree-sitter-wasms) - Register .jl in grammars, types, and the language extractor map
1 parent f6772da commit ae7ae0e

10 files changed

Lines changed: 458 additions & 4 deletions

File tree

__tests__/extraction.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3826,6 +3826,154 @@ local function run(y) return helper(y) end
38263826
});
38273827
});
38283828

3829+
// =============================================================================
3830+
// Julia (tree-sitter-julia WASM vendored; extends colbymchenry/codegraph#244)
3831+
// =============================================================================
3832+
3833+
describe('Julia Extraction', () => {
3834+
describe('Language detection', () => {
3835+
it('should detect Julia files', () => {
3836+
expect(detectLanguage('main.jl')).toBe('julia');
3837+
expect(detectLanguage('graph_utils.jl')).toBe('julia');
3838+
});
3839+
3840+
it('should report Julia as supported', () => {
3841+
expect(isLanguageSupported('julia')).toBe(true);
3842+
expect(getSupportedLanguages()).toContain('julia');
3843+
});
3844+
});
3845+
3846+
describe('Function extraction', () => {
3847+
it('should extract top-level function definitions', () => {
3848+
const code = `
3849+
function greet(name::String)
3850+
println("Hello")
3851+
end
3852+
3853+
function add(a::Int, b::Int)::Int
3854+
return a + b
3855+
end
3856+
`;
3857+
const result = extractFromSource('utils.jl', code);
3858+
const fns = result.nodes.filter((n) => n.kind === 'function');
3859+
expect(fns.find((f) => f.name === 'greet')).toBeDefined();
3860+
expect(fns.find((f) => f.name === 'add')).toBeDefined();
3861+
});
3862+
3863+
it('should extract function signature', () => {
3864+
const code = `
3865+
function process(x::Int, y::Float64)::String
3866+
return string(x + y)
3867+
end
3868+
`;
3869+
const result = extractFromSource('process.jl', code);
3870+
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'process');
3871+
expect(fn).toBeDefined();
3872+
expect(fn?.signature).toContain('x::Int');
3873+
});
3874+
3875+
it('should extract macro definitions', () => {
3876+
const code = `
3877+
macro mytime(expr)
3878+
return :(0)
3879+
end
3880+
`;
3881+
const result = extractFromSource('macros.jl', code);
3882+
const macroFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'mytime');
3883+
expect(macroFn).toBeDefined();
3884+
});
3885+
3886+
it('should extract one-line assignment functions', () => {
3887+
const code = 'has_key(d, k) = (k in keys(d))';
3888+
const result = extractFromSource('short.jl', code);
3889+
expect(result.nodes.find((n) => n.name === 'has_key' && n.kind === 'function')).toBeDefined();
3890+
});
3891+
});
3892+
3893+
describe('Struct and abstract extraction', () => {
3894+
it('should extract struct definitions without block wrapper', () => {
3895+
const code = `
3896+
struct Point
3897+
x::Float64
3898+
y::Float64
3899+
end
3900+
3901+
mutable struct Counter
3902+
value::Int
3903+
end
3904+
`;
3905+
const result = extractFromSource('types.jl', code);
3906+
const structs = result.nodes.filter((n) => n.kind === 'struct');
3907+
expect(structs.find((s) => s.name === 'Point')).toBeDefined();
3908+
expect(structs.find((s) => s.name === 'Counter')).toBeDefined();
3909+
});
3910+
3911+
it('should extract abstract type definitions', () => {
3912+
const code = `
3913+
abstract type Animal end
3914+
abstract type Shape end
3915+
`;
3916+
const result = extractFromSource('abstract.jl', code);
3917+
const abstracts = result.nodes.filter((n) => n.kind === 'interface');
3918+
expect(abstracts.find((a) => a.name === 'Animal')).toBeDefined();
3919+
expect(abstracts.find((a) => a.name === 'Shape')).toBeDefined();
3920+
});
3921+
});
3922+
3923+
describe('Module extraction', () => {
3924+
it('should extract module and nested definitions', () => {
3925+
const code = `
3926+
module SampleGraph
3927+
export greet
3928+
3929+
function greet(name::String)
3930+
println("Hello")
3931+
end
3932+
end
3933+
`;
3934+
const result = extractFromSource('mymodule.jl', code);
3935+
expect(result.nodes.find((n) => n.kind === 'module' && n.name === 'SampleGraph')).toBeDefined();
3936+
expect(
3937+
result.nodes.find(
3938+
(n) => (n.kind === 'function' || n.kind === 'method') && n.name === 'greet'
3939+
)
3940+
).toBeDefined();
3941+
});
3942+
});
3943+
3944+
describe('Import extraction', () => {
3945+
it('should extract import and using statements', () => {
3946+
const code = `
3947+
import LinearAlgebra
3948+
import Base.Math: sin, cos
3949+
using Statistics
3950+
using DataFrames: DataFrame
3951+
`;
3952+
const result = extractFromSource('imports.jl', code);
3953+
const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
3954+
expect(imports).toContain('LinearAlgebra');
3955+
expect(imports).toContain('Statistics');
3956+
});
3957+
});
3958+
3959+
describe('Call extraction', () => {
3960+
it('should extract function calls inside bodies without block', () => {
3961+
const code = `
3962+
function run(g)
3963+
out_neighbors(g, v)
3964+
sorted = topological_sort(cons)
3965+
end
3966+
`;
3967+
const result = extractFromSource('run.jl', code);
3968+
const calls = result.unresolvedReferences
3969+
.filter((r) => r.referenceKind === 'calls')
3970+
.map((r) => r.referenceName);
3971+
expect(calls).toContain('out_neighbors');
3972+
expect(calls).toContain('topological_sort');
3973+
});
3974+
});
3975+
});
3976+
38293977
// =============================================================================
38303978
// Luau (typed superset of Lua — https://luau.org)
38313979
// =============================================================================

package-lock.json

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@types/better-sqlite3": "^7.6.0",
4848
"@types/node": "^20.19.30",
4949
"@types/picomatch": "^4.0.2",
50+
"tree-sitter-julia": "^0.23.1",
5051
"typescript": "^5.0.0",
5152
"vitest": "^2.1.9"
5253
},

src/extraction/grammars.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
3737
scala: 'tree-sitter-scala.wasm',
3838
lua: 'tree-sitter-lua.wasm',
3939
luau: 'tree-sitter-luau.wasm',
40+
julia: 'tree-sitter-julia.wasm',
4041
};
4142

4243
/**
@@ -92,6 +93,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
9293
'.sc': 'scala',
9394
'.lua': 'lua',
9495
'.luau': 'luau',
96+
'.jl': 'julia',
9597
};
9698

9799
/**
@@ -155,7 +157,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
155157
// ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
156158
// 0.25 (drops nested calls/imports on every file after the first); we
157159
// vendor the upstream ABI-15 wasm instead.
158-
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
160+
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'julia')
159161
? path.join(__dirname, 'wasm', wasmFile)
160162
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
161163
const language = await WasmLanguage.load(wasmPath);
@@ -325,6 +327,7 @@ export function getLanguageDisplayName(language: Language): string {
325327
scala: 'Scala',
326328
lua: 'Lua',
327329
luau: 'Luau',
330+
julia: 'Julia',
328331
yaml: 'YAML',
329332
twig: 'Twig',
330333
unknown: 'Unknown',

src/extraction/languages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { pascalExtractor } from './pascal';
2525
import { scalaExtractor } from './scala';
2626
import { luaExtractor } from './lua';
2727
import { luauExtractor } from './luau';
28+
import { juliaExtractor } from './julia';
2829

2930
export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
3031
typescript: typescriptExtractor,
@@ -47,4 +48,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
4748
scala: scalaExtractor,
4849
lua: luaExtractor,
4950
luau: luauExtractor,
51+
julia: juliaExtractor,
5052
};

0 commit comments

Comments
 (0)