Skip to content

Troubleshooting

Quick fixes for the issues you’re most likely to hit. Run with -v first — most failures explain themselves in the verbose logs.

SymptomLikely causeFix
java: command not foundNo JDK on PATHInstall Java 11+ and add it to PATH (see Installation).
Cannot find symbol type X / unresolved typesDependencies not downloadedLet 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 emptyNo entry point for WALA to anchor onVerify the project has a main(String[]) or a recognized framework entry point.
Legacy import schema errorOld analysis.json read by a new JARRegenerate analysis.json with codeanalyzer 2.3.7+ (see below).
Build fails during level-2 analysisProject doesn’t build with the default commandPass a working -b "<cmd>", or pre-build and use --no-build.
Native binary throws, JAR doesn’tStale reflect-config.jsonRegenerate native-image config (see below).
analysis.json truncated / corruptProcess killed or out of diskCheck disk space and re-run; ensure the run completes.
--neo4j-uri ignored, graph.cypher written insteadRunning the native binary (no bundled driver)Push over Bolt from the fat JAR, java -jar (see below).
Graph empty / wrong app when reading from CLDKapplication_name doesn’t match the --app-name the graph was loaded withPass the same name in Neo4jConnectionConfig (see below).
Stale or duplicate nodes after a renameSnapshot wipe / Bolt prune are scoped per app; deletes only pruned on a full runRe-run without -t, or re-load the snapshot (see below).
Bolt push: authentication failure / wrong databaseDefault neo4j/neo4j credentials or unset NEO4J_* envSet NEO4J_PASSWORD / --neo4j-database (see below).
J_CALLS edges missing from the graphRan at level 1, or callee is an external library symbolRun -a 2 on a full (non--t) analysis (see below).

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 -v and check the build log.
  • If you used --no-build, make sure the compiled output is actually present and current.

Symbol resolution depends on library dependencies being available. If types from third-party libraries show up as simple (unqualified) names:

  • Don’t run --no-build on a project that isn’t already built — dependency download is part of the normal flow.
  • Inspect what was fetched by adding --no-clean-dependencies and looking in target/_library_dependencies/ (Maven) or build/_library_dependencies/ (Gradle).
  • For multi-module projects, point -f at the reactor root pom.xml / build.gradle. See Build integration.

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.

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:

Terminal window
./gradlew fatJar
java -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 nativeCompile

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:

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

This 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 CLDK
from cldk.analysis import AnalysisLevel
from 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.DiGraph

Checklist:

  • Match the name exactly. Remember the default: when you populated the graph without --app-name, the anchor took the base name of the -i input directory (or the literal application if 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 written graph.cypher instead of pushing (see above) — nothing reached the database.

  • Install the driver. Constructing the backend without it raises CodeanalyzerExecutionException with an install hint. Run pip install cldk[neo4j] (or pip 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_version stamped on the :JApplication node is 1.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.cypher snapshot 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 / :JAnnotation nodes 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_hash and 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.cypher snapshot, 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.

SettingFlagEnv varDefault
URI--neo4j-uriNEO4J_URI(none — without it, no live push)
User--neo4j-userNEO4J_USERNAMEneo4j
Password--neo4j-passwordNEO4J_PASSWORDneo4j
Database--neo4j-databaseNEO4J_DATABASE(server default)
  • An authentication failure usually means the run fell back to the neo4j/neo4j defaults against a server with real credentials. Export NEO4J_PASSWORD (and NEO4J_USERNAME if it isn’t neo4j) before running.
  • A “database not found” error means --neo4j-database / NEO4J_DATABASE names 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 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 no J_CALLS. Only level 2 adds the WALA call-graph edges.

  • Don’t combine -t with -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 2 analysis to (re)build J_CALLS.

  • Expect gaps at the boundary. J_CALLS is gated to both endpoints being emitted application callables. Calls into external/library targets that were never emitted as :JCallable nodes 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