Repository navigation
CLDK go - #434
CLDK go#434
Conversation
Add the Go arm to the backend-selection layer so a Go facade can dispatch on config type and cache per-language artifacts without colliding in a polyglot repo: - GoBackend = Union[CodeAnalyzerConfig, Neo4jConnectionConfig] — the same shape as JavaBackend. cango has no backend-only knob today, so Go reuses the base CodeAnalyzerConfig rather than a GoCodeAnalyzerConfig subclass. - "go": "go" in _CACHE_KEYS so cache_subdir(..., "go") resolves to <root>/go. Part of #1 (GoAnalysisBackend frontend rung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hand-written mirror of the codeanalyzer-go (cango) analysis.json, the way
the Java/TS model packages mirror their analyzers (cango is a compiled Go
binary, so there is no package to re-export from like Python does).
- models.py: GoAnalysis envelope → GoApplication → GoModule → GoType →
GoCallable, plus GoField/GoParam/GoImport/GoBodyNode/GoCallGraphEdge/
GoSpan/GoMetrics/GoAnalyzer. Validates all 14 real Eval/new-apps samples
(schema_version 2.0.0, max_level 2).
- GoType is one model with kind ∈ {struct, interface}, not a discriminated
union of per-kind subclasses like TSType: the two Go kinds carry the same
wire shape, so a union would be ceremony. Go is the SDK's first class-less,
type-centric facade — functions kept distinct from methods, no class.
- Go-native fields carried first-class the way TS carries is_async/is_tsx:
GoParam.is_variadic (...T), GoBodyNode.is_goroutine (go f()),
GoCallable.error_channel ((T, error) idiom), GoCallable.source_file.
- extra="ignore" (matching TS codellm-devkit#386) so an additive cango release parses
without an SDK edit; _NullSafeBase coerces a JSON null collection to its
empty default before validation (Go's nil-slice idiom), honoring both the
= [] field style and default_factory.
- No projections.py yet: the overview models are out of this train's SDK3
scope and add purely additively later.
Part of #1 (GoAnalysisBackend frontend rung).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The abstract base both Go backends implement, parameterising the shared AnalysisBackend with Go's models and setting the graph vocabulary prefixes N="Go" / P="GO_". - GoAnalysisBackend(AnalysisBackend[GoApplication, GoModule, GoType, GoCallable, GoField, GoParam]) — 14 lifecycle/artifact slots inherited, 5 Go-specific accessors added: get_functions (package-level funcs, no base slot), get_callers/get_callees (keyed on callable id, returning full GoCallGraphEdge — lossless prov/weight), get_source, get_call_graph_json. - Class-less, type-centric: the facade users see is get_types/get_functions/ get_methods_of, never "class". The inherited class-named slots (get_all_classes etc.) are filled only to instantiate and map TypeT=GoType (struct + interface); never user-facing. - ARTIFACT_LAYER_UNAVAILABLE: the artifact/dependency/config layer is absent from a local L1/L2 analysis.json, so the local backend raises it (Java precedent — [] is reserved for "pass ran, found nothing") while the Neo4j backend answers for real from its Artifact/Package/ConfigKey nodes. - __init__.py is a package marker for now; GoAnalysis is exported once the facade lands. Pure contract — no bodies; first behavioral test is test_go_backend_contract asserting both concrete backends implement all 19 abstractmethods. Part of #1 (GoAnalysisBackend frontend rung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GoCodeanalyzer(GoAnalysisBackend): run cango as a subprocess, read the v2
analysis.json, validate into GoAnalysis, and answer all 19 abstractmethods
from the in-memory tree. Verified end-to-end on a real Go module and against
the 573-callable echo sample (47 modules, 110 types, 195 funcs, 473 edges).
- Binary discovery: $CODEANALYZER_GO_BIN -> codeanalyzer_go wheel bin_path()
-> CodeanalyzerExecutionException with install hint (wheel arm lights up at
pin time; build now, pin last).
- CLI: -i/--app-name/-a <1|2>/--analysis-schema 2/--skip-tests/--eager/-t.
--analysis-schema 2 is mandatory — cango defaults to the legacy v1 shape the
v2 models reject. Level capped at 2 (no L3/L4).
- Output: stdout pipe when no cache dir; else persist to <cache>/go and reuse
unless eager/target_files force a re-run.
- Indexing: types keyed by durable can:// id (a Go type carries no signature,
and the types{} short-name key isn't unique across packages); callables by
signature. No id->key indirection — Go's call-graph endpoints already ARE
callable ids.
- get_callers/get_callees filter the flat call_graph by dst/src == id,
returning full GoCallGraphEdge (prov/weight preserved).
- get_source slices module.source by span.bytes, which are UTF-8 BYTE offsets:
a non-ASCII module is sliced on its encoded bytes (cached once) and decoded,
the way TS _Spanned.code does. Character-index slicing drifted by one on the
first multibyte sample.
- Artifact/dependency/config six raise ARTIFACT_LAYER_UNAVAILABLE: that layer
is not emitted by a local L1/L2 run (Java precedent — [] means proved-empty).
Part of #1 (GoAnalysisBackend frontend rung).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GoNeo4jBackend(GoAnalysisBackend): a drop-in alternative to GoCodeanalyzer answering the same 19-method surface over a GO_-prefixed graph a `cango --emit neo4j` run populated out of band. Verified for PARITY against the local backend on the live cobra graph (17 types, 106 funcs, 458 edges, 18 modules — identical type-id/signature sets, matching callers/callees incl. prov/weight, matching get_source). - Lazy per-subtree fetch, NOT an eager GoApplication rebuild: Go graphs reach enterprise scale (a 525k-node / 156k-edge app was measured), so each accessor issues one indexed Cypher query touching only what it needs. get_callers(id) is a single GO_CALLS lookup; nothing materialises the whole tree. Parity is therefore proven by test, not guaranteed by construction. - config.py re-exports Neo4jConnectionConfig; reconstruct.py has the pure props-map -> model builders (span_json / metrics_json are JSON strings on the wire, parsed here); neo4j_backend.py holds the lazy accessors. - Every query scoped to can://<app>/ + the :GoCanNode marker, so a shared graph with several apps or a sibling-language subgraph is never scanned in. - get_source slices :GoModule.source (which the projection carries) by the node's span_json byte offsets, UTF-8-aware, matching the local backend. - neo4j driver is a lazy import (optional extra cldk[neo4j]); the module imports fine without it and raises a clear install message on construction. - Artifact/dependency/config six RAISE here too, same as the local backend: the Go projector declares :Artifact/:Package/:ConfigKey but does not yet emit them (0 such nodes in cobra), so the layer is reserved-but-empty on both projections. Flip to querying for real when the projector emits it. Part of #1 (GoAnalysisBackend frontend rung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The thin public query layer. Picks a backend by config type (Neo4jConnectionConfig -> GoNeo4jBackend, else CodeAnalyzerConfig -> GoCodeanalyzer) and forwards every query. Verified on BOTH backends: the local one on a real Go module, the Neo4j one on the live cobra graph. - SDK1 made real at the user layer: the public surface is get_types/get_type/ get_functions/get_methods_of/get_method/get_fields — NO class-named method anywhere a caller can see (verified: zero 'class' in the public API). These forward to the backend's inherited class-named slots, which exist only to satisfy the shared ABC. - Types, their methods and fields are addressed by the type's durable can:// id (a Go type has no unique short name across packages); get_callers/get_callees by the callable's can:// id, returning the full GoCallGraphEdge (SDK2). - Package-level functions kept distinct from receiver methods (no module-as-class hack). - __init__.py now exports GoAnalysis. Part of #1 (GoAnalysisBackend frontend rung).
Wire the Go facade into the public entry point so the issue's acceptance gate passes: CLDK(language="go").analysis(project_path=...) returns a populated type table + call graph. Verified on both backends (local on a real module, Neo4j on the live cobra graph), and the legacy shim emits the expected DeprecationWarning. - CLDK.go(project_path, *, analysis_level, target_files, eager, backend) mirroring CLDK.java/python/typescript; backend selected by config type. - Legacy CLDK(language="go").analysis(...) shim routes to CLDK.go() with a CodeAnalyzerConfig (or the passed neo4j_config); source_code mode refused as for the other languages. Return type and deprecation message widened to include Go. - CLAUDE.md supported-languages table gets the Go row (required in the same change): cango -a 1..2 only, --analysis-schema 2 mandatory, version pin deferred (no release cut), lazy-Cypher Neo4j backend, no projections.py. Part of #1 (GoAnalysisBackend frontend rung).
The four test tiers for the Go frontend. CI-safe tiers (contract, mocked, models) run with no external resources; the live tiers skip cleanly when the binary / Neo4j env are absent. Verified: 61 passed / 11 skipped with nothing available; 72 passed / 0 skipped with cango + the live cobra graph present. - test_go_backend_contract.py: both backends parameterise the generic ABC (N=Go, P=GO_), every generic + Go-specific abstractmethod is implemented on both, and signatures don't drift. This is the 'GoAnalysisBackend instantiates' gate at the class level. - test_go_models.py: the fixture validates; GoType is one model with a kind field; _NullSafeBase coerces null collections; the Go-native fields exist. - test_go_analysis.py: facade with the cango subprocess mocked (fixture analysis.json written into the cache dir). Asserts the public surface has NO class-named method (SDK1), functions distinct from methods, call graph dangling-free, callers/callees keyed on id, get_source slices, artifact six raise. - test_go_e2e.py: real cango on a throwaway go.mod module; skipif no binary. - test_go_neo4j_live.py: GoNeo4jBackend vs GoCodeanalyzer parity on the same application (type-id sets, function-sig sets, edge counts, module counts, callees incl. prov/weight); skipif no CLDK_TEST_GO_NEO4J_* env. The artifact six raise on the Neo4j backend too. - fixture_analysis.json: a 2 KB real cango v2 payload (one struct + method, one package func, one call edge). Part of #1 (GoAnalysisBackend frontend rung).
A standalone Go Quick Start in the README and a runnable example script, both
showing the type-centric, class-less facade (get_types / get_functions /
get_methods_of, never get_classes) on both backends. Every snippet verified
against real cango output.
- README: a "### Go Quick Start" subsection (full type-centric walk: types +
receiver methods, package-level functions, get_source, call graph with
prov/weight; the read-only Neo4j backend; a cango/$CODEANALYZER_GO_BIN note).
Also adds Go to the language picker, the Supported Languages table, and the
deprecation note.
- examples/go_quickstart.py: a parametrized runner over a given Go module.
Local (cango) backend plus an optional read-only Neo4j backend (runs when
CLDK_GO_NEO4J_* are set, skips cleanly otherwise). An -a/--level {1,2} option
(default 2 = symbol table + call graph; 1 = symbol table only, where it says
the call graph is not computed rather than printing a misleading empty one).
cango has no L3/L4, so only levels 1 and 2 are offered.
Part of #1 (GoAnalysisBackend frontend rung).
…ng to 1) GoCodeanalyzer mapped the level with a hand-rolled dict keyed on the string "call_graph", but the facade passes the AnalysisLevel.call_graph ENUM, whose value is "call graph" (a space, not an underscore). The lookup missed and fell back to the default 1, so `-a 1` always reached cango regardless of the requested level — every project came back with no call graph (0 edges), even at analysis_level=call_graph. Replace the dict with the SDK's shared analyzer_level() helper (the same one the TS backend uses), which accepts the enum / name / value and RAISES on an unknown level instead of defaulting to 1 — its docstring exists precisely to close "the level that silently becomes 1" defect. _argv still caps at 2 (cango has no L3/L4). The bug hid because the test fixture has no cross-function calls (0 edges at any level looked correct); a real app (dae: 3241 edges) exposed it. Add a regression test asserting the requested level reaches cango as the right `-a` integer. Part of #1 (GoAnalysisBackend frontend rung).
Give go_quickstart.py an -e/--eager flag that forces a fresh cango run, ignoring any cached analysis.json under <project>/.codeanalyzer/go. Needed because the local backend reuses a cached analysis by existence, not by level: a cache written by an earlier shallower run (e.g. `-a 1`) is served for a deeper request (`-a 2`), so the call graph comes back empty. --eager is the escape hatch; the underlying level-blind cache reuse in GoCodeanalyzer (and the TS backend it mirrors) is a separate, still-open issue. Part of #1 (GoAnalysisBackend frontend rung).
…ested GoCodeanalyzer decided whether to reuse a cached analysis.json by whether the file EXISTED, never by its level. So a cache written by an earlier shallower run (e.g. `-a 1`) was served for a deeper request (`-a 2`), and the call graph came back empty — the 0-edges symptom seen on a real app (dae) whenever a level-1 cache was present. - needs_run now also re-runs when the cached max_level is below the requested level. A cache at or above the request is reused as-is: the schema is additive (L1 subset of L2), so a deeper cache already contains everything a shallower request needs (and re-running would waste work). A missing/unreadable/malformed cache re-runs rather than trusting it. The request is compared capped at 2 (cango's ceiling), so a request above 2 never re-runs against a level-2 cache. - Regression tests: a shallow cache is NOT reused for a deeper request (re-runs), and a deeper cache IS reused for a shallower one (no re-run). Both count cango invocations via a level-aware mock — no binary needed. Distinct from the earlier level->-a mapping fix (069bf72); this is cache reuse. The TS backend shares the same existence-only check and is still affected. Part of #1 (GoAnalysisBackend frontend rung).
update to the latest cldk
…neo4j # Conflicts: # README.md
…acade-l1l2-neo4j Python sdk goanalysisbackend go facade l1l2 neo4j
Removed --analysis-schema option
…sts/models/go Add comprehensive Go unit tests driven by the committed real analysis (tests/resources/go/analysis.json — the can://go/litestream project, 55 modules, 959 edges), replacing one-edge-toy coverage: - tests/models/go/: model tests moved here to match the sibling tests/models/<lang> layout; parse the real corpus (envelope, a real ReplicaClient struct round-trip) plus inline shape/nil-slice checks. The redundant toy-fixture validation is dropped (no model test keeps a local toy JSON, per convention). - test_go_analysis.py: a litestream_facade fixture runs the whole accessor surface over the real corpus — get_callers/get_callees/get_source against measured real ids, 959-edge dangling-free call graph. The toy fixture is kept only for the level/cache regression tests that mutate max_level. - test_go_e2e.py: a live path that unzips litestream.zip and runs the real cango binary on the full app (skips without a binary). - tests/resources/go/: the committed analysis.json, litestream.zip source, and a README rewritten to match the single-L2-file reality. 79 passed, 7 skipped (the live Neo4j tier).
test(go): leverage the real litestream corpus; move model tests to te…
sinha108
left a comment
There was a problem hiding this comment.
tests/analysis/go/test_go_analysis.py still loads tests/analysis/go/fixture_analysis.json. .gitignore excludes that file (*.json), so it isn't in this PR. On a clean checkout of e9d9669, the Go tests give 4 failed and 8 errors: every test using the go_analysis fixture, the _level_aware_run helper or _capture.
The suggestions below point those tests at the committed litestream corpus (GO_ANALYSIS) and drop the missing fixture. Apply them all at once with Commit suggestions in the Files changed tab.
Verified on a fresh clone of this PR, with cango built from codeanalyzer-go main, comparing every test's outcome before and after:
- Go tests (
tests/analysis/go,tests/models/go): 67 passed / 4 failed / 8 errors / 7 skipped → 79 passed / 7 skipped. All 12 failing tests now pass, no test went from passing to anything else, and the 7 skips are the live-Neo4j tests, skipped for lack of a database both times. - Full SDK suite: 1861 passed / 4 failed / 8 errors → 1873 passed, 0 failed, 0 errors.
Separate from this (not part of these suggestions): get_source still returns the wrong text for a method declared in a different file from its receiver type, because neither Go backend reads source_file. We'll fix that in follow-up python-sdk work after this PR merges.
Co-authored-by: Saurabh Sinha <sinha108@gmail.com>
Co-authored-by: Saurabh Sinha <sinha108@gmail.com>
Co-authored-by: Saurabh Sinha <sinha108@gmail.com>
Co-authored-by: Saurabh Sinha <sinha108@gmail.com>
Co-authored-by: Saurabh Sinha <sinha108@gmail.com>
|
Thanks, all five review suggestions are in. One more thing before merge: The release workflow installs with On a fresh clone of
The 7 remaining skips are the live-Neo4j tests. Fix: run |
updates lock file
|
Thanks for the lockfile update; Why it matters This PR adds Go support, so its lockfile change should be limited to that. A broad upgrade can have consequences we can't see from here:
The upgrade itself is fine to do, but in its own PR, run against the live Neo4j and Java suites. What to do From the PR branch: git checkout f445f85 -- uv.lock # the lockfile from before b5785ac
uv lock # no --upgrade: adds only codeanalyzer-go
git diff --stat uv.lock # expect: 1 file changed, 14 insertions(+)
uv lock --check
git commit -am "Lock codeanalyzer-go without upgrading other dependencies"
git pushThe expected diff against
Verified On a fresh clone of
These are the same counts as with the upgraded lockfile, so narrowing it costs nothing. |
Adds a Go frontend to CLDK: a new
CLDK.go(...)factory, aGoAnalysisfacade, aGoAnalysisBackendABC with two interchangeable backends (cango` subprocess and read-onlyNeo4j), Go pydantic models mirroring canonical schema v2, docs, an example, and tests.
Motivation and Context
CLDK already exposes Java, Python, and TypeScript through per-language factory methods; Go was the
missing fourth language. This PR brings Go to parity with the existing frontends so the same
"disk → question about the code" workflow works on a Go module.
Go is also the SDK's first class-less, type-centric facade. A Go
typeis a struct or aninterface (never a class), and package-level functions are distinct from receiver methods.
Rather than force Go through a "module-as-class" hack, the public surface speaks
get_types/get_functions/get_methods_of— neverget_classes. The class-named slots on the backend(
get_all_classes,get_all_methods_in_class, …) are inherited only to satisfy the sharedAnalysisBackendABC and are never user-facing; the facade translates them to the type-centricvocabulary so the abstraction never leaks to a caller.
Design decisions carried over from the Go-SDK planning:
projections.py(overview models) — out of SDK3 scope.GoTypeis a single class, not a discriminated union;kinddistinguishes struct vs.interface.
CodeAnalyzerConfig(GoBackend = Union[CodeAnalyzerConfig, Neo4jConnectionConfig]) becausecangohas no backend-only knob today — the same shape asJavaBackend. AGoCodeAnalyzerConfigsubclass is added only ifcangolater grows one.codeanalyzer-gowheel; until the firstrelease is cut, the binary is resolved out-of-band via
$CODEANALYZER_GO_BIN, so the versionpin is deferred.
How Has This Been Tested?
Yes — including against a real Go corpus (the Litestream project, a 525k-node / 156k-edge
graph at enterprise scale), not just synthetic fixtures. The suite under
tests/analysis/go/andtests/models/go/covers:test_public_surface_has_no_class_named_methodasserts the public API neverexposes a
class-named accessor; type/function/method/field navigation, call graph, callers/callees keyed on
can://ids, andget_sourcespan slicing.for a shallower one (schema is additive,
L1 ⊆ L2), and the requested level is passed through tocango(capped at 2 — Go has no L3/L4). Level mapping goes throughanalyzer_level()to avoidthe silent-default-to-1 bug a hand-rolled dict caused.
test_go_backend_contractconfirms both backends subclass the ABC,implement every abstract method, and match signatures, so the local and Neo4j backends answer the
facade identically.
call-graph edge counts, and symbol-table module counts all match across the two backends (parity
proven by test, since the Neo4j backend does lazy per-subtree Cypher rather than an eager
rebuild).
encoded bytes and decoded back.
run (and on the current Neo4j projection) rather than returning an ambiguous empty list, mirroring
the Java precedent where
[]is reserved for "the pass ran and found nothing."The live Neo4j tests (
test_go_neo4j_live.py) run against a graph populated out of band.Breaking Changes
None. This is purely additive. Existing Java / Python / TypeScript entry points are untouched.
The only changes to shared code are additive:
"go"added to_CACHE_KEYS, a newGoBackendunion, and a new
gobranch in the deprecatedCLDK(language).analysis(...)compat shim (whichnow also returns
GoAnalysis).Types of changes
Checklist
Additional context
New public API
Pass a
Neo4jConnectionConfigasbackend=to query a pre-populatedGO_-prefixed graphread-only over Bolt — the backend is chosen by the config type, and queries lazily hit the graph so
it scales to large modules without materializing the whole program.
Files
cldk/analysis/go/(go_analysis.py,backend.py,codeanalyzer/codeanalyzer.py,neo4j/neo4j_backend.py,neo4j/reconstruct.py).cldk/models/go/(schema v2 mirror;GoTypeas a single class).cldk/core.py(CLDK.go, plus thegobranch in the legacyanalysis()shim).cldk/analysis/commons/backend_config.py("go"cache key,GoBackendunion).README.md(Go Quick Start),CLAUDE.md(language table row),examples/go_quickstart.py.tests/analysis/go/,tests/models/go/,tests/resources/go/.Known limitations (intentional, documented)
cangoemits analysis levels 1–2 only; slicing / CFG / CDG / DDG accessors are absent ratherthan present-and-erroring.
raise (see above).
codeanalyzer-gowheel is not yet released, so the version pin is deferred and the binary isresolved via
$CODEANALYZER_GO_BINfor now.