diff --git a/public/wasm/tree-sitter-java.wasm b/public/wasm/tree-sitter-java.wasm new file mode 100644 index 0000000..45022a9 Binary files /dev/null and b/public/wasm/tree-sitter-java.wasm differ diff --git a/src/app/api/analyze/route.ts b/src/app/api/analyze/route.ts index 0600dc7..c6e578e 100644 --- a/src/app/api/analyze/route.ts +++ b/src/app/api/analyze/route.ts @@ -22,6 +22,7 @@ const EXT: Record = { go: "go", rust: "rs", sql: "sql", + java: "java", }; function normalizePath(p: string): string { diff --git a/src/components/ui/CodeWorkspace.tsx b/src/components/ui/CodeWorkspace.tsx index fa53d67..eab67c2 100644 --- a/src/components/ui/CodeWorkspace.tsx +++ b/src/components/ui/CodeWorkspace.tsx @@ -6,7 +6,7 @@ import { saveGraph } from "@/lib/graphs"; import type { Graph } from "@/lib/analysis/types"; import { graphSchema } from "@/lib/validation"; -const LANGUAGES = ["python", "javascript", "typescript", "go", "rust", "sql"]; +const LANGUAGES = ["python", "javascript", "typescript", "go", "rust", "sql", "java"]; const EXT: Record = { python: "py", @@ -15,6 +15,7 @@ const EXT: Record = { go: "go", rust: "rs", sql: "sql", + java: "java", }; const PROJECT_EXTS: Record = { @@ -24,6 +25,7 @@ const PROJECT_EXTS: Record = { go: [".go"], rust: [".rs"], sql: [".sql"], + java: [".java"], }; const IGNORE_DIR = @@ -163,6 +165,29 @@ CREATE TABLE post_tags ( FOREIGN KEY (post_id) REFERENCES posts (id), FOREIGN KEY (tag_id) REFERENCES tags (id) ); +`, + java: `public class App { + public static void main(String[] args) { + int[] data = load(); + save(transform(data)); + } + + public static int[] load() { + return read(); + } + + public static int[] transform(int[] data) { + return clean(data); + } + + public static int[] clean(int[] data) { + return data; + } + + public static void save(int[] x) { + write(x); + } +} `, }; diff --git a/src/lib/analysis/analyzers/analyzers.test.ts b/src/lib/analysis/analyzers/analyzers.test.ts index be85613..46165ed 100644 --- a/src/lib/analysis/analyzers/analyzers.test.ts +++ b/src/lib/analysis/analyzers/analyzers.test.ts @@ -5,6 +5,7 @@ import { typescriptAnalyzer } from "./typescript"; import { goAnalyzer } from "./go"; import { rustAnalyzer } from "./rust"; import { sqlAnalyzer } from "./sql"; +import { javaAnalyzer } from "./java"; import type { Graph, LanguageAnalyzer, SourceFile } from "../types"; function run( @@ -493,3 +494,47 @@ CREATE TABLE user_roles ( ]); }); }); + +describe("java", () => { + test("call graph and inheritance inside java files", async () => { + const graph = await run(javaAnalyzer, [ + [ + "com/example/App.java", + `package com.example; + import com.example.Service; + + public class App extends BaseApp { + public static void main(String[] args) { + Service service = new Service(); + service.execute(); + } + } + class BaseApp {}` + ], + [ + "com/example/Service.java", + `package com.example; + + public class Service { + public void execute() { + log(); + } + private void log() {} + }` + ] + ]); + + // Check class nodes + expect(hasNode(graph, "class::com/example/App.java::App")).toBe(true); + expect(hasNode(graph, "class::com/example/Service.java::Service")).toBe(true); + + // Check class inheritance (App extends BaseApp) + expect(hasEdge(graph, "class::com/example/App.java::App", "class::com/example/App.java::BaseApp", "extends")).toBe(true); + + // Check call graph edge (execute calls log) + expect(hasEdge(graph, "com/example/Service.java::execute", "com/example/Service.java::log", "calls")).toBe(true); + + // Check call graph edge between files (main calls execute) + expect(hasEdge(graph, "com/example/App.java::main", "com/example/Service.java::execute", "calls")).toBe(true); + }); +}); diff --git a/src/lib/analysis/analyzers/java.ts b/src/lib/analysis/analyzers/java.ts new file mode 100644 index 0000000..5f02c28 --- /dev/null +++ b/src/lib/analysis/analyzers/java.ts @@ -0,0 +1,63 @@ +import type { LanguageAnalyzer } from "../types"; +import { analyzeProjectWith, type LangSpec } from "./shared"; +import type Parser from "web-tree-sitter"; + +function classBases(node: Parser.SyntaxNode): string[] { + const superclass = node.namedChildren.find((c) => c.type === "superclass"); + if (!superclass) return []; + const typeNode = superclass.namedChildren[0]; + if (typeNode) { + if (typeNode.type === "generic_type") { + const baseNode = typeNode.childForFieldName("type"); + if (baseNode) return [baseNode.text]; + } + return [typeNode.text]; + } + return []; +} + +function resolveModule( + fromFile: string, + specifier: string, + paths: Set, +): string | null { + if (specifier.endsWith(".*")) { + return null; + } + const relPath = specifier.replace(/\./g, "/") + ".java"; + for (const p of paths) { + if (p.endsWith(relPath)) { + return p; + } + } + return null; +} + +const spec: LangSpec = { + language: "java", + wasm: "tree-sitter-java.wasm", + funcDefQuery: ` + (method_declaration) @def + (constructor_declaration) @def + `, + callQuery: ` + (method_invocation name: (identifier) @callee) + `, + importQuery: ` + (import_declaration (scoped_identifier) @mod) + (import_declaration (identifier) @mod) + `, + classQuery: ` + (class_declaration) @class + (interface_declaration) @class + `, + funcDefTypes: new Set(["method_declaration", "constructor_declaration"]), + classNodeTypes: new Set(["class_declaration", "interface_declaration"]), + classBases, + resolveModule, +}; + +export const javaAnalyzer: LanguageAnalyzer = { + language: spec.language, + analyzeProject: (files) => analyzeProjectWith(spec, files), +}; diff --git a/src/lib/analysis/registry.ts b/src/lib/analysis/registry.ts index ffb7dde..5f79a59 100644 --- a/src/lib/analysis/registry.ts +++ b/src/lib/analysis/registry.ts @@ -5,6 +5,7 @@ import { typescriptAnalyzer } from "./analyzers/typescript"; import { goAnalyzer } from "./analyzers/go"; import { rustAnalyzer } from "./analyzers/rust"; import { sqlAnalyzer } from "./analyzers/sql"; +import { javaAnalyzer } from "./analyzers/java"; const analyzers: LanguageAnalyzer[] = [ pythonAnalyzer, @@ -13,6 +14,7 @@ const analyzers: LanguageAnalyzer[] = [ goAnalyzer, rustAnalyzer, sqlAnalyzer, + javaAnalyzer, ]; const registry = new Map(analyzers.map((a) => [a.language, a]));