Skip to content

Commit 67b4da2

Browse files
committed
refactor(java,python): formalize backend contracts as shared ABCs
Mirror the TypeScript TSAnalysisBackend pattern for Java and Python, in anticipation of Neo4j/Cypher backends for those languages too: - cldk/analysis/java/backend.py: JavaAnalysisBackend (36-method surface); JCodeanalyzer now subclasses it. - cldk/analysis/python/backend.py: PythonAnalysisBackend (21-method surface); PyCodeanalyzer now subclasses it. Both facades type their `backend` attribute against the ABC, so a future alternative backend can be selected without touching the facade. Added contract tests for each (subclass, abstract/not-instantiable, fully-implemented, and that the ABC covers every method the facade delegates to).
1 parent 16b7440 commit 67b4da2

8 files changed

Lines changed: 456 additions & 4 deletions

File tree

cldk/analysis/java/backend.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
################################################################################
2+
# Copyright IBM Corporation 2026
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
################################################################################
16+
17+
"""The Java analysis backend contract.
18+
19+
:class:`JavaAnalysis` is a (mostly) thin façade that delegates its static-analysis queries to a
20+
*backend*. Today the only backend is :class:`~cldk.analysis.java.codeanalyzer.JCodeanalyzer`
21+
(in-memory pydantic / NetworkX over the codeanalyzer JSON); this ABC formalizes the surface the
22+
façade depends on so an alternative backend (e.g. a forthcoming Neo4j/Cypher backend, mirroring
23+
the TypeScript :class:`~cldk.analysis.typescript.neo4j.TSNeo4jBackend`) can be dropped in and
24+
selected without touching the façade.
25+
26+
The contract is enforced by the type system and at instantiation time rather than matching only by
27+
convention. Note the façade also calls Tree-sitter directly for a few parsing/sanitization helpers
28+
(e.g. ``is_parsable``, ``get_raw_ast``); those are not part of the backend contract — only the
29+
analysis queries the façade routes through ``self.backend`` are.
30+
"""
31+
32+
from __future__ import annotations
33+
34+
from abc import ABC, abstractmethod
35+
from typing import Dict, List, Tuple, Union
36+
37+
import networkx as nx
38+
39+
from cldk.models.java.models import (
40+
JApplication,
41+
JCallable,
42+
JCallableParameter,
43+
JComment,
44+
JCompilationUnit,
45+
JCRUDOperation,
46+
JField,
47+
JMethodDetail,
48+
JType,
49+
)
50+
51+
# A CRUD query row: the owning type + callable and the operations found within it.
52+
CRUDRow = Dict[str, Union[JType, JCallable, List[JCRUDOperation]]]
53+
54+
55+
class JavaAnalysisBackend(ABC):
56+
"""Abstract base every Java analysis backend implements.
57+
58+
A backend owns all indexing and query logic for a Java application (symbol table, call graph,
59+
class/method/field navigation, entry points, CRUD operations, comments/docstrings); the
60+
:class:`JavaAnalysis` façade delegates to it. Implementations must return the canonical
61+
``cldk.models.java`` pydantic objects (or the documented NetworkX / dict / list shapes) so
62+
backends are behaviorally interchangeable.
63+
"""
64+
65+
# -----[ application / whole-program ]-----
66+
@abstractmethod
67+
def get_application_view(self) -> JApplication:
68+
"""The whole application view."""
69+
70+
@abstractmethod
71+
def get_symbol_table(self) -> Dict[str, JCompilationUnit]:
72+
"""The per-file symbol table, keyed by file path."""
73+
74+
@abstractmethod
75+
def get_compilation_units(self) -> List[JCompilationUnit]:
76+
"""All compilation units."""
77+
78+
@abstractmethod
79+
def get_java_file(self, qualified_class_name: str) -> str:
80+
"""The file path declaring a class."""
81+
82+
@abstractmethod
83+
def get_java_compilation_unit(self, file_path: str) -> JCompilationUnit:
84+
"""The compilation unit for a file path."""
85+
86+
# -----[ call graph ]-----
87+
@abstractmethod
88+
def get_call_graph(self) -> nx.DiGraph:
89+
"""NetworkX DiGraph of the application's call edges."""
90+
91+
@abstractmethod
92+
def get_call_graph_json(self) -> str:
93+
"""The call graph serialized as JSON."""
94+
95+
@abstractmethod
96+
def get_all_callers(self, target_class_name: str, target_method_signature: str, using_symbol_table: bool) -> Dict:
97+
"""Callers of a method."""
98+
99+
@abstractmethod
100+
def get_all_callees(self, source_class_name: str, source_method_signature: str, using_symbol_table: bool) -> Dict:
101+
"""Callees of a method."""
102+
103+
@abstractmethod
104+
def get_class_call_graph(self, qualified_class_name: str, method_name: str | None = None) -> List[Tuple[JMethodDetail, JMethodDetail]]:
105+
"""Call-graph edges reachable from a class (or one of its methods)."""
106+
107+
@abstractmethod
108+
def get_class_call_graph_using_symbol_table(self, qualified_class_name: str, method_signature: str | None = None) -> List[Tuple[JMethodDetail, JMethodDetail]]:
109+
"""Call-graph edges reachable from a class, computed from the symbol table only."""
110+
111+
# -----[ classes / methods / fields ]-----
112+
@abstractmethod
113+
def get_all_classes(self) -> Dict[str, JType]:
114+
"""Every class, keyed by qualified name."""
115+
116+
@abstractmethod
117+
def get_class(self, qualified_class_name: str) -> JType:
118+
"""A single class by qualified name."""
119+
120+
@abstractmethod
121+
def get_all_sub_classes(self, qualified_class_name: str) -> Dict[str, JType]:
122+
"""Classes that extend/implement the given class."""
123+
124+
@abstractmethod
125+
def get_all_nested_classes(self, qualified_class_name: str) -> List[JType]:
126+
"""The classes declared inside a class."""
127+
128+
@abstractmethod
129+
def get_extended_classes(self, qualified_class_name: str) -> List[str]:
130+
"""The base classes a class extends."""
131+
132+
@abstractmethod
133+
def get_implemented_interfaces(self, qualified_class_name: str) -> List[str]:
134+
"""The interfaces a class implements."""
135+
136+
@abstractmethod
137+
def get_all_methods_in_application(self) -> Dict[str, Dict[str, JCallable]]:
138+
"""All methods grouped by their owning class qualified name."""
139+
140+
@abstractmethod
141+
def get_all_methods_in_class(self, qualified_class_name: str) -> Dict[str, JCallable]:
142+
"""The methods of a class."""
143+
144+
@abstractmethod
145+
def get_method(self, qualified_class_name: str, method_signature: str) -> JCallable:
146+
"""A single method of a class."""
147+
148+
@abstractmethod
149+
def get_method_parameters(self, qualified_class_name: str, method_signature: str) -> List[JCallableParameter]:
150+
"""The parameters of a method."""
151+
152+
@abstractmethod
153+
def get_all_constructors(self, qualified_class_name: str) -> Dict[str, JCallable]:
154+
"""The constructors of a class."""
155+
156+
@abstractmethod
157+
def get_all_fields(self, qualified_class_name: str) -> List[JField]:
158+
"""The fields of a class."""
159+
160+
# -----[ entry points ]-----
161+
@abstractmethod
162+
def get_all_entry_point_methods(self) -> Dict[str, Dict[str, JCallable]]:
163+
"""Methods identified as application entry points."""
164+
165+
@abstractmethod
166+
def get_all_entry_point_classes(self) -> Dict[str, JType]:
167+
"""Classes identified as application entry points."""
168+
169+
# -----[ CRUD operations ]-----
170+
@abstractmethod
171+
def get_all_crud_operations(self) -> List[CRUDRow]:
172+
"""All CRUD operations across the application."""
173+
174+
@abstractmethod
175+
def get_all_create_operations(self) -> List[CRUDRow]:
176+
"""All create operations."""
177+
178+
@abstractmethod
179+
def get_all_read_operations(self) -> List[CRUDRow]:
180+
"""All read operations."""
181+
182+
@abstractmethod
183+
def get_all_update_operations(self) -> List[CRUDRow]:
184+
"""All update operations."""
185+
186+
@abstractmethod
187+
def get_all_delete_operations(self) -> List[CRUDRow]:
188+
"""All delete operations."""
189+
190+
# -----[ comments / docstrings ]-----
191+
@abstractmethod
192+
def get_all_comments(self) -> Dict[str, List[JComment]]:
193+
"""All comments across the application, keyed by file."""
194+
195+
@abstractmethod
196+
def get_comment_in_file(self, file_path: str) -> List[JComment]:
197+
"""The comments in a file."""
198+
199+
@abstractmethod
200+
def get_comments_in_a_class(self, qualified_class_name: str) -> List[JComment]:
201+
"""The comments in a class."""
202+
203+
@abstractmethod
204+
def get_comments_in_a_method(self, qualified_class_name: str, method_signature: str) -> List[JComment]:
205+
"""The comments in a method."""
206+
207+
@abstractmethod
208+
def get_all_docstrings(self) -> List[Tuple[str, JComment]]:
209+
"""All docstring-style comments across the application."""
210+
211+
@abstractmethod
212+
def remove_all_comments(self, src_code: str) -> str:
213+
"""Strip all comments from the given source code."""

cldk/analysis/java/codeanalyzer/codeanalyzer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import networkx as nx
2929

3030
from cldk.analysis import AnalysisLevel
31+
from cldk.analysis.java.backend import JavaAnalysisBackend
3132
from cldk.analysis.commons.treesitter import TreesitterJava
3233
from cldk.models.java import JGraphEdges
3334
from cldk.models.java.enums import CRUDOperationType
@@ -37,7 +38,7 @@
3738
logger = logging.getLogger(__name__)
3839

3940

40-
class JCodeanalyzer:
41+
class JCodeanalyzer(JavaAnalysisBackend):
4142
"""A class for building the application view of a Java application using Codeanalyzer.
4243
4344
Args:

cldk/analysis/java/java_analysis.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from cldk.models.java import JApplication
5858
from cldk.models.java.models import JCRUDOperation, JComment, JCompilationUnit, JMethodDetail, JType, JField
5959
from cldk.analysis.java.codeanalyzer import JCodeanalyzer
60+
from cldk.analysis.java.backend import JavaAnalysisBackend
6061

6162

6263
class JavaAnalysis:
@@ -154,7 +155,7 @@ def __init__(
154155
self.target_files = target_files
155156
self.treesitter_java: TreesitterJava = TreesitterJava()
156157
# Initialize the analysis analysis_backend
157-
self.backend: JCodeanalyzer = JCodeanalyzer(
158+
self.backend: JavaAnalysisBackend = JCodeanalyzer(
158159
project_dir=self.project_dir,
159160
source_code=self.source_code,
160161
eager_analysis=self.eager_analysis,

cldk/analysis/python/backend.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
################################################################################
2+
# Copyright IBM Corporation 2026
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
################################################################################
16+
17+
"""The Python analysis backend contract.
18+
19+
:class:`PythonAnalysis` is a thin façade that delegates every query to a *backend*. Today the only
20+
backend is :class:`~cldk.analysis.python.codeanalyzer.PyCodeanalyzer` (in-memory pydantic /
21+
NetworkX over ``analysis.json``); this ABC formalizes the surface the façade depends on so an
22+
alternative backend (e.g. a forthcoming Neo4j/Cypher backend, mirroring the TypeScript
23+
:class:`~cldk.analysis.typescript.neo4j.TSNeo4jBackend`) can be dropped in and selected without
24+
touching the façade.
25+
26+
The contract is enforced by the type system and at instantiation time rather than matching only
27+
by convention. Backend-specific lifecycle (caches, drivers) is intentionally not part of it.
28+
"""
29+
30+
from __future__ import annotations
31+
32+
from abc import ABC, abstractmethod
33+
from typing import Dict, List, Tuple
34+
35+
import networkx as nx
36+
37+
from cldk.models.python import (
38+
PyApplication,
39+
PyCallable,
40+
PyClass,
41+
PyClassAttribute,
42+
PyModule,
43+
)
44+
45+
46+
class PythonAnalysisBackend(ABC):
47+
"""Abstract base every Python analysis backend implements.
48+
49+
A backend owns all indexing and query logic for a Python application; the
50+
:class:`PythonAnalysis` façade is a one-line-delegation shim over it. Implementations must
51+
return the canonical ``cldk.models.python`` pydantic objects (or the documented
52+
NetworkX / dict / list shapes) so backends are behaviorally interchangeable.
53+
"""
54+
55+
# -----[ application / whole-program ]-----
56+
@abstractmethod
57+
def get_application_view(self) -> PyApplication:
58+
"""The whole application view (symbol table + call graph)."""
59+
60+
@abstractmethod
61+
def get_symbol_table(self) -> Dict[str, PyModule]:
62+
"""The per-file symbol table, keyed by module file path."""
63+
64+
@abstractmethod
65+
def get_modules(self) -> List[PyModule]:
66+
"""All modules."""
67+
68+
@abstractmethod
69+
def get_python_module(self, file_path: str) -> PyModule | None:
70+
"""The module for a file path."""
71+
72+
@abstractmethod
73+
def get_python_file(self, qualified_class_name: str) -> str | None:
74+
"""The file path declaring the given symbol."""
75+
76+
# -----[ call graph ]-----
77+
@abstractmethod
78+
def get_call_graph(self) -> nx.DiGraph:
79+
"""NetworkX DiGraph of the application's call edges."""
80+
81+
@abstractmethod
82+
def get_call_graph_json(self) -> str:
83+
"""The application serialized as JSON."""
84+
85+
@abstractmethod
86+
def get_all_callers(self, target_class_name: str, target_method_declaration: str) -> Dict:
87+
"""Callers of a method, with the connecting call-graph edge metadata."""
88+
89+
@abstractmethod
90+
def get_all_callees(self, source_class_name: str, source_method_declaration: str) -> Dict:
91+
"""Callees of a method, with the connecting call-graph edge metadata."""
92+
93+
@abstractmethod
94+
def get_class_call_graph(self, qualified_class_name: str, method_signature: str | None = None) -> List[Tuple[str, str]]:
95+
"""Call-graph edges reachable from a class (or one of its methods)."""
96+
97+
# -----[ classes ]-----
98+
@abstractmethod
99+
def get_all_classes(self) -> Dict[str, PyClass]:
100+
"""Every class, keyed by signature."""
101+
102+
@abstractmethod
103+
def get_class(self, qualified_class_name: str) -> PyClass | None:
104+
"""A single class by signature."""
105+
106+
@abstractmethod
107+
def get_all_nested_classes(self, qualified_class_name: str) -> List[PyClass]:
108+
"""The classes declared inside a class."""
109+
110+
@abstractmethod
111+
def get_all_sub_classes(self, qualified_class_name: str) -> Dict[str, PyClass]:
112+
"""Classes that extend the given class."""
113+
114+
@abstractmethod
115+
def get_extended_classes(self, qualified_class_name: str) -> List[str]:
116+
"""The base types a class extends."""
117+
118+
# -----[ methods / fields ]-----
119+
@abstractmethod
120+
def get_all_methods_in_application(self) -> Dict[str, Dict[str, PyCallable]]:
121+
"""All methods grouped by their owning class signature."""
122+
123+
@abstractmethod
124+
def get_all_methods_in_class(self, qualified_class_name: str) -> Dict[str, PyCallable]:
125+
"""The methods of a class."""
126+
127+
@abstractmethod
128+
def get_method(self, qualified_class_name: str, qualified_method_name: str) -> PyCallable | None:
129+
"""A single method of a class."""
130+
131+
@abstractmethod
132+
def get_method_parameters(self, qualified_class_name: str, qualified_method_name: str) -> List[str]:
133+
"""The parameter names of a method."""
134+
135+
@abstractmethod
136+
def get_all_constructors(self, qualified_class_name: str) -> Dict[str, PyCallable]:
137+
"""The constructors of a class."""
138+
139+
@abstractmethod
140+
def get_all_fields(self, qualified_class_name: str) -> List[PyClassAttribute]:
141+
"""The attributes/fields of a class."""

0 commit comments

Comments
 (0)