python-sdk v2.0.0-rc.3 #66
rahlk
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
TypeScript and Java now answer the same query surface Python has: addressing, per-callable control
and data flow, slices, call-graph and value-flow predicates, entrypoints, and the repository-artifact
layer — identically whether you read an
analysis.jsonor an attached Neo4j graph, including on thepaths where a backend has to refuse.
Four legs of work: TypeScript on schema v2 and its query surface, Java on schema v2 (driven by
the analyzer wheel, with degraded runs reported rather than silent) and its query surface. Design
records:
docs/design/specs/2026-09-06-leg-2.5-typescript.mdanddocs/design/specs/2026-09-06-leg-3-java.md.Both analyzers moved with it. codeanalyzer-java 3.1.0 connects the level-4 port lattice, so Java's
interprocedural value questions answer for the first time, and adds config-read provenance and the
entrypoint report. codeanalyzer-typescript 1.5.0 corrects source spans to UTF-8 byte offsets and
gives declaration-merged names one id per facet, so a class and an interface of the same name are both
reachable instead of one shadowing the other.
Where a backend cannot answer, it says so and says why. Every such refusal is measured from the data in
front of it, never from an analyzer version string, so a re-emitted graph starts answering on its own.
What remains unanswerable is listed under Known limitations rather than returned as an empty
result.
Breaking
get_call_graph()nodes are"<type fqn>.<signature>"strings, not(signature, klass)tuples.Take the owning class from
cg.nodes[key]["method_detail"].klassrather than splitting the key.source_codemode is gone. Analyze the project directory instead.p.Outer.m(int).$anon$0).Without it, sibling callables collide. Take class keys from
get_all_classes()rather than composing them.GraphSchemaMismatchinstead ofanswering every query with zero rows: codeanalyzer-java 3.0.1 and codeanalyzer-typescript 1.3.0. Re-emit;
there is no in-place upgrade.
TSNeo4jBackendalso speaks a graph vocabulary that shares nothing with 0.4.3's.JGraphEdgesandTSCallEdgeare now{src, dst, prov, weight}; Java'scalling_linesis a sorted list of absolute file lines; Java'sget_config_keys()is keyed by theartifact-relative key; Java's
get_test_methods()reads the analyzer's annotations rather than re-parsingsource;
TSCallablelostpath/call_sites/accessed_symbols/local_variables/code_start_lineandTSModulelostfile_path/module_name, since v2 keys modules by path and stores source once;TSCallableOverview.from_callabletakes a required keyword-onlypath.TypeScriptAnalysis.get_entry_point_methodsandget_service_entry_point_methods, which onlyever raised — the working entrypoint accessors below replace them.
TSSpan.bytesare UTF-8 byte offsets, on every node at every level(codeanalyzer-typescript 1.5.0, cants#179). They were UTF-16 code units, which Python sliced as code points
and the Neo4j projection sliced as bytes — three units that agreed only on ASCII.
TSCallable.codeandevery accessor built on it (
get_source,get_method_bodies,describe,locate(...).source) now decodethe module's UTF-8 bytes, the way Java's
JCompilationUnit.slice()already did: encoded once per module, aplain index when the file is ASCII. On a non-ASCII file, text read through 1.3.0/1.4.0 was wrong and is now
right — re-emit rather than compare offsets across versions. Note that
schema_versionstays2.0.0through this break: the contract version is not a signal that a breaking change landed.
Added
TypeScriptAnalysis— 29 accessors, each withPythonAnalysis'ssignature and semantics. Addressing (
locate,locate_many,resolve_callable,resolve_value,get_source,describe,has_resolution_edges); per-callable graphs and dataflow (get_cfg/get_cdg/get_ddg,slice_backward/slice_forward/backward_cone,reaches,callers_of/callees_of,paths_between/call_paths_between,flows_to_call/flows_to_argument); entrypoints(
get_entrypoints,get_entrypoint_classes,get_entrypoint_coverage,get_config_readers); and the fiverepository-artifact getters. Both backends answer identically, including on the miss paths.
JavaAnalysis— 38 accessors, each withPythonAnalysis's signature andsemantics. Addressing (
locate,locate_many,resolve_callable,resolve_value,get_source,describe,has_resolution_edges); per-callable graphs and dataflow (get_cfg/get_cdg/get_ddg,slice_backward/slice_forward/backward_cone,reaches,callers_of/callees_of,paths_between/call_paths_between,flows_to_call/flows_to_argument); entrypoints and the bulkprojections (
get_entrypoints,get_entrypoint_classes,get_entrypoint_coverage,get_callables_overview,get_method_bodies,get_decorated_callables,get_callsites_for,get_external_symbols); the six repository-artifact getters; and the type-kind leaf accessorsget_interfaces/get_enums/get_enum_members/get_records. Both backends answer identically,including on the miss paths.
cldk.models.java.JCallableOverviewandJClassOverview, the projections those bulk accessorsreturn. They carry the addressable
"<type fqn>.<signature>"key, never acan://id.NotImplementedErrornow answer (#366), at thesignature they have always been published with:
get_imports()(the project's distinct importtargets, sorted — the Neo4j projection aggregates a module's imports per target, so file order is
not recoverable and the set is what both backends can give),
get_variables()(each callable'slocal variables, keyed by the
"<type fqn>.<signature>"call-graph key and ordered by(line, name); fields and parameters keep their own accessors, and an unexpected keyword nowraises
TypeErrorinstead of being ignored),get_class_hierarchy()(anx.DiGraph, subclass →supertype, each edge carrying
type="EXTENDS"or"IMPLEMENTS"— read off each declaration's ownbase_types/interfaces, which is why out-of-project supertypes are in it),get_methods_with_annotations()(grouped by the spelling the caller passed, matched by the J-5marker rule, each entry
{class, signature, method_name, body}),get_call_targets()(thedeclared names some call site actually writes — simple-name matching, no overload resolution) and
get_calling_lines()(sorted, distinct absolute file lines, off the call graph's owncalling_lines). Both backends answer identically; none of the six issues any new Cypher.get_service_entry_point_classes/get_service_entry_point_methodsandremove_all_commentsstill raise.
graph. The level now reaches the analyzer, which it never did before.
javainstall extra.pip install "cldk[java]"brings the analyzer and its bundled JVM;pip install "cldk[all]"reproduces the previous behaviour. Barecldkno longer carries either. The Pythonand TypeScript analyzers are still installed unconditionally; #340 moves them into extras of their own.
without them the analyzer emits a declared-only graph and still reports the level. The SDK now surfaces the
analyzer's own warnings and records them beside the payload, so a cached run still knows. The graph is
returned either way.
cldk.models.typescript.TSClassOverview, the class-level projectionget_entrypoint_classesreturns.Changed
slice_forward,paths_between,flows_to_callandflows_to_argumentrefused on Java because codeanalyzer-java emitted the level-4 portlattice disconnected from the statement dependence graph — a
formal_inhad out-degree zero, so everyforward answer was empty whatever the program did. codeanalyzer-java 3.0.3 joins the two layers
(codeanalyzer-java#227), and the SDK's guard reads the data rather than a version, so all four answer: a
value can be followed out of a parameter, across call boundaries and into a callee's parameter, with each
hop labelled
data/argument/return/control. Measured on the whole of daytrader8: 5,083ddgedges cross between the port lattice and the statement graph in all four directions, where there were none.
To get this, re-analyse (or re-emit your Neo4j graph) with the pinned analyzer; an older graph stays
attachable and keeps refusing, as does any analysis run under
--l3-engine wala. The same release dropsddgedges whose endpoint was never emitted as a body node (codeanalyzer-java#228), soget_ddg()overanalysis.jsonand over Neo4j now report the same edges — 10,430 on daytrader8, set for set, against a5,434/5,347 split before.
get_config_uses(),get_config_readers(key)andget_unresolved_config_reads()return real edges insteadof
[], andget_entrypoint_coverage()returns the pass's own record instead ofentrypoint_report_unavailable. The two config tiers are surfaced, not flattened:prov == ["literal"]is a string literal at the call site (codeanalyzer-java#233),
"dataflow"is a key reached over the L3 DDGor the L4 call graph (#237) — a derived answer, weaker evidence, and it widens with the analysis level. On a
PyConfigReadthe list is every tier attempted, so["literal", "dataflow"]means the dataflow tier ranand still could not name the key. Measured on daytrader8: 13 resolved uses (all
["literal"]) and 16unresolved reads —
["literal"]at level 1,["literal", "dataflow"]at level 4; the graph collapses those16 into 8 edges, since
J_READS_CONFIG_UNRESOLVEDis discriminated by(key, reason)and carries no site.To get this, re-analyse (or re-emit your Neo4j graph) with the pinned analyzer; an older analysis
keeps refusing, and the probe is measured from the data rather than from a version string — see the known
limitation below for what it measures and why it cannot be the config layer's own absence.
codeanalyzer-java2.4.1 → 3.1.0,codeanalyzer-typescript0.4.3 → 1.5.0.in
_jdk.py, and the release workflow's jar injection are gone; noJAVA_HOMEis read or set, and no JDK isdownloaded. The published wheel drops from about 35 MB to 320 KB.
AnalysisBackend, so each answers the shared artifact,dependency and configuration accessors.
comment accessors, and TypeScript's
get_extended_classes/get_implemented_interfaceswhen the relationshiptype is absent — and its
get_imports,get_exports,get_method_parametersandget_unresolved_config_readson a graph emitted before codeanalyzer-typescript 1.4.0, which carries noneof the data they read (below). The remaining documented gaps, and where the two backends legitimately differ,
are listed in
docs/agent-api-reference.md.get_imports,get_exports,get_method_parametersandget_unresolved_config_readsanswerover Neo4j, on a graph emitted by codeanalyzer-typescript 1.4.0 or newer: the projection gained
TS_IMPORTS/TS_RE_EXPORTSedges,:TSModule.exports_json,:TSCallable.parameters_jsonandTS_READS_CONFIG_UNRESOLVED(codeanalyzer-typescript#182). Parameters also reachTSCallable.parametersandexports reach
TSModule.exportson a rebuilt node. A 1.3.0 graph is still attachable and those fourstill refuse there rather than answer an empty that would read as a fact; the decision is measured from the
application's own data — is any carrier present — never from the analyzer's version string, so a re-emitted
graph starts answering with no SDK change.
TSImport/TSExportgained the analyzer's newresolved_module. Migration: re-emit withcodeanalyzer-typescript>=1.5.0 --emit neo4j.graph rather than taken from the release notes.
:TSCallable.codeis no longer one line short over Neo4j(cants#179) — the four
xfail(strict=True)marks that pinned it now XPASS and are removed. Declarationmerging mints one id per facet (cants#177), so a
class X+interface Xpair is two nodes and bothare served, where before one facet was lost to whichever declaration the emitter saw last; on
superset-frontend that is 7 signatures, and no node carries two declaration labels any more. The analyzer
also refuses a missing or non-directory input instead of exiting 0 (cants#181) and no longer clones the whole
envelope per module (cants#180); neither had an SDK workaround to remove.
quantified path's far end, a slice's reached body nodes, and every interior node of a variable-length or
shortest-path walk (
all(n IN nodes(p) …)on the slice, path and reachability queries). A statement thatmatched one endpoint by a signature two applications both declare, or that walked through an intermediate it
never predicated, could previously return the other application's node.
LocateResult.bodyis a language-neutralBodyRef, socldk/analysis/commons/no longer imports alanguage package for it.
AmbiguousNamenow names only the ways out that are actually open — on all three languages. TheNarrow it with …sentence offersin_class=/in_module=only when the caller has not already passedthat keyword and the listed matches disagree on it, so two overloads of one class, or two
__init__s ofone class, are no longer told to narrow by a keyword that provably cannot split them. The last clause is
always offered and is language-specific:
more of the dotted pathon Python and TypeScript, and on Javathe full signature, exactly as one of the listed matches spells it(more of the dotted path cannotseparate two overloads).
AmbiguousName.candidatesis unchanged. Python'sresolve_callable,callers_of,callees_ofandresolve_withintherefore produce a different message for the same inputthan in rc.2; assert on
.candidates, not on the sentence.cldk/analysis/python/tocldk/analysis/commons/;Python re-imports every name unchanged.
Fixed
get_cfg/get_cdg/get_ddgon the Python Neo4j backend no longer drop self-loop edges. Theper-callable query bound the containment relationship twice, so Cypher's relationship-uniqueness rule
silently discarded every edge whose endpoints are the same body node — a statement that reads a
variable it also redefines. 64,702 such edges exist on the odoo reference graph; one callable returned
28,146 of its 28,394.
totalwas counted from the same match, so the short page reported itselfcomplete. (#349)
JavaAnalysis.get_method_parameters()is annotatedList[JCallableParameter], which is what it has alwaysreturned.
get_call_graph()on Java no longer re-parses each method body once per edge — 145.7s to 41.0s on a4,100-file project.
Two documentation errors:
EntrypointCoverage.unresolvedis adict[str, int], not alist[str]; and theget_config_keys()example used a key form that never worked.get_source()on the in-process TypeScript backend named the application by itscan://id where the Neo4jbackend named it plainly; both now name the application.
get_external_symbols()andget_source()overNeo4j scoped externals by the
can://typescript/prefix alone, dropping any external a JavaScript moduleowns; both now carry the two-prefix scope.
Known limitations
JCallable.codeis the declaration slice where the JSON's is the body block, and the graphcannot recover the body block (codeanalyzer-java#176).
TS_IMPORTSfoldsevery binding between a module pair into one edge of sorted sets, so an entry's alias,
import_kindand spanare not recoverable, and the emitter drops a relative specifier that resolved to no emitted module. Exports,
parameters and unresolved config reads are lossless, except that the config-read edge carries no
siteandcollapses sites sharing a
(callee, key, reason)triple.get_config_keys()is still keyed by acan://id on Python and TypeScript, where Java now uses theartifact-relative key (#346).
slice_forward,paths_between,flows_to_callandflows_to_argumentstill raise on ananalysis emitted before codeanalyzer-java 3.0.3, or by any version under
--l3-engine wala: no dependenceedge leaves a
formal_inthere, so every forward answer would be empty whatever the program does. A Neo4jgraph emitted by 3.0.1 or 3.0.2 is still attachable (the floor is 3.0.1) and still refuses — re-emit it.
flows_to_argumentis not per-argument precise. codeanalyzer-java feeds every actual of a callsite from the one statement containing it, not from the reaching definition of that argument, so on a
reached call site every argument answers
Truetogether. Paths are complete; per-argument precision is notwhat the analyzer promises.
a cached
analysis.jsonis reused whatever wrote it, and a 3.0.x Neo4j graph is still attachable, the floorbeing 3.0.1 —
get_entrypoint_coverage()returnsentrypoint_report_unavailablerather than fabricatedcoverage, and
get_config_uses()/get_config_readers()/get_unresolved_config_reads()raise ratherthan answer
[]. The probe is one fact measured from the data: 3.1.0 writes an entrypoint report on everyrun and 3.0.x writes none of the three overlays. It deliberately is not the config layer's own absence,
which is ambiguous — the analyzer writes
config_uses/config_reads_unresolvedonly when non-empty, andthe graph declares
J_USES_CONFIG/J_READS_CONFIG_UNRESOLVEDonly once an edge exists, so a clean 3.1.0analysis of a project that reads no configuration carries neither and must still answer.
get_entrypoints()and
get_entrypoint_classes()carry the real marks at every generation, as doesget_config_keys().get_external_symbols()answers over Neo4j and raises locally: the analyzer homes out-of-projectcall targets only under
--external-calls, which--emit neo4jforces and a local run does not.switchbody-node kind, which is outsideSliceNode.KINDS(that vocabulary iscodeanalyzer-python's, and Python has no switch statement). It is reported as the analyzer spells it.
get_ddg()over Neo4j carries 276points-toedges (on daytrader8) that the sameanalyzer's
analysis.jsondoes not, and that is a difference of what each source was asked.codeanalyzer-java 3.1.0 makes the level-4
points-tolayer depend on--external-calls, which--emit neo4jforces on and which the SDK's local run does not pass; 3.0.3 produced the same10,430 edges either way. Measured on daytrader8, same tree, four runs: 3.0.3
-a 4→ 10,430(1,134
points-to); 3.1.0-a 4→ 10,154 (858); 3.1.0-a 4 --external-calls→ 10,430, set forset identical to the graph; 3.1.0
--emit neo4j→ 10,430. The graph is a strict superset and thepayload has nothing the graph lacks, so
slice_forwardand the other forward walks can reachfurther over Neo4j. Reported upstream: the flag is documented as controlling only whether
out-of-project call targets are homed as
external_symbols.codeanalyzer-java#231's config-read and entrypoint-report halves and not its comment half:
:JCommentis adeclared label with zero nodes on a graph emitted by it, and only a declaration's
docstringreachesthe graph. So
remove_all_commentskeeps raising, and the file-keyed comment accessors keep refusing overNeo4j while the javadoc-only ones narrow, exactly as before.
All reactions