Troubleshooting
Quick fixes for the issues you’re most likely to hit. Run with -v first — most failures explain themselves in the verbose logs.
Reference table
Section titled “Reference table”| Symptom | Likely cause | Fix |
|---|---|---|
java: command not found | No JDK on PATH | Install Java 11+ and add it to PATH (see Installation). |
Cannot find symbol type X / unresolved types | Dependencies not downloaded | Let codeanalyzer build (don’t use --no-build on an uncompiled project), or ensure dependencies are resolvable. Use --no-clean-dependencies to inspect what was fetched. |
call_graph is empty | No entry point for WALA to anchor on | Verify the project has a main(String[]) or a recognized framework entry point. |
| Legacy import schema error | Old analysis.json read by a new JAR | Regenerate analysis.json with codeanalyzer 2.3.7+ (see below). |
| Build fails during level-2 analysis | Project doesn’t build with the default command | Pass a working -b "<cmd>", or pre-build and use --no-build. |
| Native binary throws, JAR doesn’t | Stale reflect-config.json | Regenerate native-image config (see below). |
analysis.json truncated / corrupt | Process killed or out of disk | Check disk space and re-run; ensure the run completes. |
--neo4j-uri ignored, graph.cypher written instead | Running the native binary (no bundled driver) | Push over Bolt from the fat JAR, java -jar (see below). |
| Graph empty / wrong app when reading from CLDK | application_name doesn’t match the --app-name the graph was loaded with | Pass the same name in Neo4jConnectionConfig (see below). |
| Stale or duplicate nodes after a rename | Snapshot wipe / Bolt prune are scoped per app; deletes only pruned on a full run | Re-run without -t, or re-load the snapshot (see below). |
Bolt push: authentication failure / wrong database | Default neo4j/neo4j credentials or unset NEO4J_* env | Set NEO4J_PASSWORD / --neo4j-database (see below). |
J_CALLS edges missing from the graph | Ran at level 1, or callee is an external library symbol | Run -a 2 on a full (non--t) analysis (see below). |
Empty call graph
Section titled “Empty call graph”WALA traverses the call graph outward from entry points. If none are found, the graph can be empty even though the symbol table is complete.
- Confirm the project actually has an entry point — a
main(String[])or one of the supported framework patterns. - Confirm the project built. WALA needs compiled classes; if the build silently failed, there’s nothing to analyze. Re-run with
-vand check the build log. - If you used
--no-build, make sure the compiled output is actually present and current.
Unresolved types
Section titled “Unresolved types”Symbol resolution depends on library dependencies being available. If types from third-party libraries show up as simple (unqualified) names:
- Don’t run
--no-buildon a project that isn’t already built — dependency download is part of the normal flow. - Inspect what was fetched by adding
--no-clean-dependenciesand looking intarget/_library_dependencies/(Maven) orbuild/_library_dependencies/(Gradle). - For multi-module projects, point
-fat the reactor rootpom.xml/build.gradle. See Build integration.
Legacy import schema
Section titled “Legacy import schema”When merging incremental updates into an existing analysis.json, you may see:
Existing analysis.json uses legacy import schema (imports as strings). Regenerate analysis with codeanalyzer 2.3.7 or newer.
Older analyzers emitted imports as bare strings; from 2.3.7 they are structured objects ({ path, is_static, is_wildcard }). The merge guard refuses to mix the two. Fix: delete the old analysis.json and regenerate it with a current JAR before re-running incremental updates.
Native-image exceptions
Section titled “Native-image exceptions”If the GraalVM native binary throws random exceptions that java -jar does not, the reflection config is likely out of date. Regenerate it with the native-image agent and rebuild:
./gradlew fatJarjava -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image-config \ -jar build/libs/codeanalyzer-2.3.7.jar -i <sample-project> -a 2 -v./gradlew nativeCompileNeo4j graph output
Section titled “Neo4j graph output”These cover the --emit neo4j projection — the live Bolt push, the graph.cypher snapshot, and reading the graph back from the CLDK Python SDK.
--neo4j-uri is ignored and a graph.cypher file is written instead
Section titled “--neo4j-uri is ignored and a graph.cypher file is written instead”The Neo4j driver is deliberately not bundled into the GraalVM native binary — it’s loaded reflectively so native-image can prune the driver and Netty. The prebuilt codeanalyzer native binary therefore cannot open a Bolt connection: when you pass --neo4j-uri it degrades gracefully to writing graph.cypher and logs a warning, rather than pushing anything live.
If you actually wanted the live, incremental push, run it from the fat JAR instead:
NEO4J_PASSWORD=secret java -jar codeanalyzer-2.3.7.jar \ -i /path/to/project -a 2 \ --emit neo4j --app-name daytrader8 \ --neo4j-uri bolt://localhost:7687 --neo4j-user neo4jThis is the producer side of the producer/consumer split: the JAR runs out-of-band (a CI step, or a Kubernetes Job / CronJob) and pushes app-scoped subgraphs to a shared Neo4j over Bolt. If you only need the snapshot — for review, version control, or an air-gapped load — the native binary’s degraded behaviour is fine; just load the file afterwards with cypher-shell < graph.cypher.
Graph is empty, or you get the wrong app, when reading from CLDK
Section titled “Graph is empty, or you get the wrong app, when reading from CLDK”The Neo4j-backed SDK is a pure read-only client — it never builds the graph, it only queries one that a codeanalyzer --emit neo4j job has already populated. Every query is scoped to a single application by application_name, which must equal the --app-name the graph was loaded with. A mismatch silently returns nothing, because the query anchors on a :JApplication node that doesn’t exist.
from cldk import CLDKfrom cldk.analysis import AnalysisLevelfrom cldk.analysis.commons.backend_config import Neo4jConnectionConfig
analysis = CLDK.java( analysis_level=AnalysisLevel.call_graph, backend=Neo4jConnectionConfig( uri="bolt://localhost:7687", username="neo4j", password="neo4j", application_name="daytrader8", # == the CLI --app-name ),)symbol_table = analysis.get_symbol_table() # Dict[str, JCompilationUnit]cg = analysis.get_call_graph() # networkx.DiGraphChecklist:
-
Match the name exactly. Remember the default: when you populated the graph without
--app-name, the anchor took the base name of the-iinput directory (or the literalapplicationif there was no input). Confirm what’s actually in the database:MATCH (a:JApplication) RETURN a.name, a.schema_version -
Confirm the graph was populated at all. If the producer ran the native binary with
--neo4j-uri, it may have writtengraph.cypherinstead of pushing (see above) — nothing reached the database. -
Install the driver. Constructing the backend without it raises
CodeanalyzerExecutionExceptionwith an install hint. Runpip install cldk[neo4j](orpip install neo4j). -
Check the schema version. The read-back expects an emitter at 2.4.0 or newer (projection fixes landed in 2.4.1). The
schema_versionstamped on the:JApplicationnode is1.0.0; keep the analyzer that writes the graph and the SDK that reads it on compatible versions.
Stale or duplicate nodes after a rename or delete
Section titled “Stale or duplicate nodes after a rename or delete”Both emit modes are scoped per application, and they handle vanished files differently:
- The
graph.cyphersnapshot opens with a wipe that deletes only this app’s prior subgraph —MATCH (a:JApplication {name: <app-name>})then detaches its units and descendants — before re-loading the full truth. Shared:JPackage/:JAnnotationnodes are intentionally left in place so other apps that reference them aren’t disturbed. Re-loading the snapshot is therefore always a clean slate for that one app. - The live Bolt push is incremental. It diffs each compilation unit’s
content_hashand replaces only changed units’ subgraphs. Orphan pruning — deleting units whose source file has vanished — runs only on a full analysis. A targeted run (-t) skips pruning so it can touch just the files you named, which means a deleted or renamed file’s old nodes can linger.
So if you renamed or deleted a class and its old nodes are still in the graph:
-
Re-run a full analysis (no
-t) over Bolt so orphan pruning removes the vanished unit:Terminal window NEO4J_PASSWORD=secret java -jar codeanalyzer-2.3.7.jar \-i /path/to/project -a 2 \--emit neo4j --app-name daytrader8 \--neo4j-uri bolt://localhost:7687 --neo4j-user neo4j -
Or regenerate and re-load the
graph.cyphersnapshot, whose scoped wipe clears this app’s subgraph wholesale before reloading.
Renames produce duplicates (not just staleness) precisely because the old FQN and the new one are distinct node keys — the upsert can’t know they’re the same logical unit. A full run reconciles them.
Bolt connection or authentication failures
Section titled “Bolt connection or authentication failures”Credentials and target database resolve from a flag first, then the matching environment variable, then a built-in default. Prefer the NEO4J_* environment variables — especially NEO4J_PASSWORD — so secrets stay off the command line and out of shell history.
| Setting | Flag | Env var | Default |
|---|---|---|---|
| URI | --neo4j-uri | NEO4J_URI | (none — without it, no live push) |
| User | --neo4j-user | NEO4J_USERNAME | neo4j |
| Password | --neo4j-password | NEO4J_PASSWORD | neo4j |
| Database | --neo4j-database | NEO4J_DATABASE | (server default) |
- An
authentication failureusually means the run fell back to theneo4j/neo4jdefaults against a server with real credentials. ExportNEO4J_PASSWORD(andNEO4J_USERNAMEif it isn’tneo4j) before running. - A “database not found” error means
--neo4j-database/NEO4J_DATABASEnames a database that doesn’t exist on the server. Leave it unset to use the server default, or create the database first. - The analyzer only needs write access. The SDK consumers that read the graph can — and in production should — use separate read-only credentials, which is the point of the producer/consumer split.
J_CALLS edges are missing
Section titled “J_CALLS edges are missing”J_CALLS is the level-2 call-graph edge between two application callables. If your queries find types and methods but no J_CALLS relationships:
-
Run at
-a 2. Level 1 emits the lossless symbol-table subgraph with noJ_CALLS. Only level 2 adds the WALA call-graph edges. -
Don’t combine
-twith-a 2. A targeted run downgrades to level 1, so it refreshes the symbol-table subgraph but recomputes no call edges. Run a full-a 2analysis to (re)buildJ_CALLS. -
Expect gaps at the boundary.
J_CALLSis gated to both endpoints being emitted application callables. Calls into external/library targets that were never emitted as:JCallablenodes have no edge — this is documented projection-lossy behaviour, not a bug. Verify with:MATCH (a:JApplication {name: "daytrader8"})-[:J_HAS_UNIT]->(:JCompilationUnit)-[:J_DECLARES_TYPE]->(:JType)-[:J_HAS_CALLABLE]->(c:JCallable)RETURN count { (c)-[:J_CALLS]->() } AS outgoing_calls