The Java Analyzer module is a specialized static-analysis component that parses Java source files using tree-sitter and converts them into the structural nodes and call relationships consumed by the broader dependency-analysis pipeline. It is one of several language-specific analyzers (alongside the C-family, web-scripting, PHP, and Python analyzers) that plug into the language-agnostic dependency graph construction and clustering machinery.
The Java Analyzer's single responsibility is to turn a .java file's source text into two collections:
- Structural nodes — classes, abstract classes, interfaces, enums, records, annotations, and methods, each represented as a
Nodewith a fully-qualified component identifier in themodule.path::ClassName(ormodule.path::ClassName.methodName) format expected by the rest of the system. - Call relationships — directed edges (
CallRelationship) capturing inheritance, interface implementation, field-type usage, method invocation, and object creation, which are later used to build the project-wide dependency graph.
This module does not perform any file I/O, orchestration, or graph assembly itself — it is invoked by higher-level components as a pure parse-and-extract step for a single file's content.
TreeSitterJavaAnalyzer (in codewiki/src/be/dependency_analyzer/analyzers/java.py) is the sole core component of this module. It wraps the tree-sitter-java grammar to build an AST for a given file and walks that tree twice: once to extract structural nodes, and once to extract relationships between them.
analyzer = TreeSitterJavaAnalyzer(file_path, content, repo_path)
nodes = analyzer.nodes # List[Node]
relationships = analyzer.call_relationships # List[CallRelationship]A convenience function, analyze_java_file(file_path, content, repo_path), instantiates the analyzer and returns (nodes, call_relationships) as a tuple, matching the calling convention used by the other language analyzers in the Language Analyzers group.
On construction, TreeSitterJavaAnalyzer:
- Stores the file path, source content, and optional repository root (
repo_path), used to compute module-qualified identifiers. - Initializes empty
nodesandcall_relationshipslists. - Immediately runs
_analyze(), which parses the source with tree-sitter and populates both lists synchronously.
Because all work happens in __init__, callers only need to construct the analyzer (or call analyze_java_file) and then read the resulting attributes — there is no separate "run" step.
classDiagram
class TreeSitterJavaAnalyzer {
+file_path: Path
+content: str
+repo_path: str
+nodes: List
+call_relationships: List
+__init__(file_path, content, repo_path)
-_analyze()
-_extract_nodes(node, top_level_nodes, lines)
-_extract_relationships(node, top_level_nodes)
-_get_module_path()
-_get_relative_path()
-_get_component_id(name, parent_class)
-_find_containing_class(node, top_level_nodes)
-_find_variable_type(node, variable_name, top_level_nodes)
-_is_primitive_type(type_name)
}
class Node {
+id: str
+name: str
+component_type: str
+file_path: str
+relative_path: str
+source_code: str
+start_line: int
+end_line: int
+display_name: str
+component_id: str
}
class CallRelationship {
+caller: str
+callee: str
+call_line: int
+is_resolved: bool
}
TreeSitterJavaAnalyzer --> Node : produces
TreeSitterJavaAnalyzer --> CallRelationship : produces
Node and CallRelationship are shared data models defined outside this module in the Data Models and Utilities sub-module of the dependency analysis pipeline, used consistently across all language analyzers.
The analyzer performs a two-pass recursive walk over the tree-sitter parse tree: a node-extraction pass followed by a relationship-extraction pass.
flowchart TD
Start["TreeSitterJavaAnalyzer.__init__"] --> Parse["_analyze(): parse source with tree_sitter_java grammar"]
Parse --> Root["Obtain AST root node"]
Root --> Pass1["_extract_nodes(root, top_level_nodes, lines)"]
Pass1 --> Pass2["_extract_relationships(root, top_level_nodes)"]
Pass2 --> Done["nodes and call_relationships populated"]
subgraph NodeExtraction["Node Extraction (recursive)"]
Pass1 --> CheckType{"node.type?"}
CheckType -->|"class_declaration"| ClassNode["Create Node: class or abstract class"]
CheckType -->|"interface_declaration"| IfaceNode["Create Node: interface"]
CheckType -->|"enum_declaration"| EnumNode["Create Node: enum"]
CheckType -->|"record_declaration"| RecordNode["Create Node: record"]
CheckType -->|"annotation_type_declaration"| AnnoNode["Create Node: annotation"]
CheckType -->|"method_declaration"| MethodNode["Create Node: method (qualified by containing class)"]
ClassNode --> Recurse1["Recurse into children"]
IfaceNode --> Recurse1
EnumNode --> Recurse1
RecordNode --> Recurse1
AnnoNode --> Recurse1
MethodNode --> Recurse1
CheckType -->|"other"| Recurse1
end
For each AST node visited, the analyzer inspects node.type and, when it matches one of the recognized Java declaration kinds, builds a Node:
| Tree-sitter node type | Component type | Naming |
|---|---|---|
class_declaration |
class or abstract class (if the abstract modifier is present) |
Class identifier |
interface_declaration |
interface |
Interface identifier |
enum_declaration |
enum |
Enum identifier |
record_declaration |
record |
Record identifier |
annotation_type_declaration |
annotation |
Annotation identifier |
method_declaration |
method |
ContainingClass.methodName |
Each Node is assigned a component_id of the form module.path::Name (or module.path::Class.method for methods), computed via _get_component_id, which in turn derives the dotted module path from the file's location relative to repo_path (_get_module_path). Discovered top-level declarations are also cached in a top_level_nodes dictionary keyed by simple name, which is reused during relationship extraction to resolve types back to component IDs.
A second recursive walk inspects the same tree to detect five categories of relationships, each appended to call_relationships as a CallRelationship(caller, callee, call_line, is_resolved=False):
flowchart TD
Visit["Visit AST node"] --> Inherit{"class_declaration with superclass?"}
Inherit -->|"yes"| RelInherit["Relationship: Class extends BaseClass"]
Visit --> Impl{"class/enum/record with super_interfaces?"}
Impl -->|"yes"| RelImpl["Relationship: Type implements Interface (per interface)"]
Visit --> Field{"field_declaration with non-primitive type?"}
Field -->|"yes"| RelField["Relationship: ContainingClass uses FieldType"]
Visit --> Invoke{"method_invocation on known object?"}
Invoke -->|"resolved via top_level_nodes or local/field variable type"| RelInvoke["Relationship: Caller (method or class) calls TargetType"]
Visit --> Create{"object_creation_expression?"}
Create -->|"yes"| RelCreate["Relationship: ContainingClass creates CreatedType"]
RelInherit --> Recurse["Recurse into children"]
RelImpl --> Recurse
RelField --> Recurse
RelInvoke --> Recurse
RelCreate --> Recurse
- Inheritance — a
class_declarationwith asuperclassclause produces an edge from the class to its base class, unless the base type is a recognized built-in/primitive. - Interface implementation — classes, enums, and records with a
super_interfacesclause produce one edge per implemented interface. - Field type usage — each
field_declarationwhose type is not primitive/built-in produces an edge from the containing class to the field's type. - Method invocation — for
method_invocationexpressions, the analyzer resolves the receiver object's type either directly (if it matches a known top-level type name) or by searching local variable declarations and field declarations (_find_variable_type/_search_variable_declaration); the caller is the enclosing method if resolvable, otherwise the enclosing class. - Object creation —
object_creation_expressionnodes produce an edge from the containing class to the instantiated type.
All non-inheritance/implementation relationships filter out common built-in types (String, Object, List, Map, primitives, boxed primitives, etc.) via _is_primitive_type to avoid polluting the dependency graph with noise from the standard library.
_get_module_path/_get_relative_path— derive dotted module paths and repo-relative file paths usingrepo_pathwhen available, falling back to the raw file path otherwise._get_component_id— builds themodule.path::Name(ormodule.path::Class.member) identifier convention shared across all language analyzers._get_identifier_name/_get_type_name— extract simple names fromidentifier,type_identifier,generic_type, andsuperclassAST nodes._find_containing_class/_find_containing_class_name/_find_containing_method— walk up the AST parent chain to determine the enclosing declaration for a given node, used to attribute relationships to the correct caller._find_variable_type/_search_variable_declaration— resolve a local variable or field's declared type by scanning the enclosing method body and class body, enabling method-invocation edges to point at concrete types rather than variable names.
sequenceDiagram
participant Caller as "Calling Component (e.g. DependencyParser)"
participant Analyzer as "TreeSitterJavaAnalyzer"
participant TS as "tree_sitter_java grammar"
participant Models as "Node / CallRelationship"
Caller->>Analyzer: analyze_java_file(file_path, content, repo_path)
Analyzer->>TS: Parser(java_language).parse(content)
TS-->>Analyzer: AST root node
Analyzer->>Analyzer: _extract_nodes(root, top_level_nodes, lines)
Analyzer->>Models: build Node(...) for each declaration
Analyzer->>Analyzer: _extract_relationships(root, top_level_nodes)
Analyzer->>Models: build CallRelationship(...) for each edge
Analyzer-->>Caller: (nodes, call_relationships)
The Java Analyzer is invoked by the language-dispatch logic that selects an appropriate analyzer per file extension, as part of the broader Dependency Graph Construction process (notably DependencyParser). The Node and CallRelationship objects it produces are:
- Aggregated across all files and languages by
DependencyGraphBuilderto construct the full project dependency graph. - Fed into higher-level analysis performed by the Repository and Call Graph Analysis module (
AnalysisService,CallGraphAnalyzer,RepoAnalyzer) for call-graph traversal and repository-wide dependency insight. - Ultimately consumed by the documentation generation pipeline in the parent
backend-coremodule, whose entry points (DocumentationGenerator,AgentOrchestrator) rely on accurate structural and relationship data to produce module documentation such as this file.
Because relationships produced here are marked is_resolved=False, downstream graph-construction logic is responsible for resolving simple type names to fully-qualified component IDs across files — the Java Analyzer only guarantees locally-consistent names and line numbers within the file it parses.
The Java Analyzer follows the same structural contract (analyze_<language>_file(file_path, content, repo_path) -> (List[Node], List[CallRelationship])) as its siblings under Language Analyzers:
- C Family Analyzers — C, C++, and C# analyzers.
- Web Scripting Analyzers — JavaScript and TypeScript analyzers.
- PHP Analyzer — PHP analyzer with namespace resolution.
- Python Analyzer — Python AST-based analyzer.
This uniform contract allows the dependency analysis pipeline to treat all languages interchangeably once each analyzer has produced its nodes and relationships, regardless of the underlying grammar or parsing strategy used internally.