Examples
Five questions a static search cannot answer, answered on real fixtures in Java, Python, and TypeScript. Every response below is actual tool output.
Three fixtures, one schema
Each language ships a six-file fixture built from the same shapes — a service, a repository, a route, an injected dependency, and one run-time lookup. The Java fixture goes deepest on dispatch and advice; the Python and TypeScript fixtures cover framework wiring.
| Fixture | Path | Crawl result | Exercises |
|---|---|---|---|
| Java | tests/fixtures/java/dynamic |
6 files · 43 nodes · 61 edges | interface dispatch, AspectJ advice, decorators |
| Python | tests/fixtures/python/sample |
6 files · 49 nodes · 65 edges | FastAPI routes, Depends() DI, importlib |
| TypeScript | tests/fixtures/typescript/sample |
6 files · 62 nodes · 81 edges | NestJS + Express routes, DI, dynamic import() |
The Java fixture is reproduced below and drives examples 1–5. The Python and TypeScript equivalents appear in the per-language section that follows.
public interface Payment {
void pay(double amount);
}
public class Checkout {
private final Payment payment;
public Checkout(Payment payment) {
this.payment = payment;
}
public void checkout(double amount) {
payment.pay(amount); // which pay()?
}
}
@Aspect
public class AuditAspect {
@Pointcut("execution(* com.acme.*.pay(..))")
public void paymentCall() {}
@Around("paymentCall()")
public Object auditPayment(JoinPoint jp) { ... }
@Before("execution(* com.acme.Checkout.checkout(..))")
public void beforeCheckout() {}
}
@Service
public class DecoratedService {
@Transactional(readOnly = true)
@Cacheable("orders")
public String describe(String id) { ... }
}
Index it with
crawl_project({ path: ".../tests/fixtures/java/dynamic" }). The
crawl reports 6 files, 43 nodes, 61 edges, 2 dynamic-dispatch edges, and 4
advice edges. Response bodies below are trimmed to the fields that matter.
Per-language examples
Tool names and response shapes do not change with the language. What changes is
what each grammar lets the parser see — so these three fixtures are chosen to
show different surfaces. Responses are actual output with
filePath values trimmed.
Python · FastAPI routes and Depends() wiring
router = APIRouter()
@router.get("/orders/{order_id}")
def read_order(order_id: str,
service: OrderService = Depends(get_service)):
return service.repo.find(order_id)
@router.post("/orders")
def create_order(...):
return {"id": service.place(order_id, amount)}
@injectable
class OrderService:
def __init__(self, repo: OrderRepository) -> None:
self.repo = repo
{
"source": { "id": "module:app.api", "kind": "module" },
"target": { "id": "module:app.api#read_order" },
"framework": "router",
"httpMethod": "GET",
"path": "/orders/{order_id}",
"line": 13
},
{
"source": { "id": "module:app.api" },
"target": { "id": "module:app.api#create_order" },
"framework": "router",
"httpMethod": "POST",
"path": "/orders",
"line": 18
}
// constructor injection
app.service.OrderService
INJECTS app.repository.OrderRepository
framework: "dependency-injector"
injectedName: "repo" · line 15
// framework-supplied parameter
module:app.api
INJECTS app.service.OrderService
framework: "fastapi"
injectedName: "service" · line 19
// importlib.import_module("app.plugins." + name)
module:app.dynamic#load_plugin
REFLECTS __reflect__:app.plugins.
mechanism: "importlib.import_module" · line 6
// entry points — CLI and HTTP in one index
module:app.main cli __main__
module:app.main#main cli main
module:app.api#create_order http POST /orders
module:app.api#read_order http GET /orders/{order_id}
trace_execution("app.service.OrderService#place") reaches
app.repository.OrderRepository#create (line 19) and
app.service.OrderService#audit (line 20) at depth 1, then stops at
the boundary ext:builtins. The framework field on a
route reports the decorator receiver — router here, because the
handler hangs off an APIRouter instance — while a
Depends() parameter reports fastapi.
TypeScript · one endpoint, two frameworks
const app = express();
app.get("/orders/:id", (req, res) => {
res.json(service.place(req.params.id));
});
@Controller("orders")
export class OrderController {
constructor(private readonly service: OrderService) {}
@Get(":id")
read(@Param("id") id: string) {
return this.service.place(id);
}
}
// Express — inline handlers get synthetic ids
module:src.routes
ROUTES module:src.routes#<anonymous@7>
framework: "express" · GET /orders/:id · line 6
// NestJS — decorators fold controller + method paths
src.controller.OrderController
ROUTES #read framework: "nestjs"
GET /orders/:id · line 7
src.controller.OrderController
ROUTES #create framework: "nestjs"
POST /orders · line 12
4 routes · 2 frameworks · 1 logical resource
src.controller.OrderController
INJECTS src.service.OrderService
framework: "ts-di" · injectedName: "service"
src.service.OrderService
INJECTS src.repository.OrderRepository
framework: "ts-di" · injectedName: "repo"
// await import(`./plugins/${name}`)
module:src.dynamic#loadPlugin
REFLECTS __reflect__:./plugins/${name}
mechanism: "import()" · line 1
depth 1 OrderRepository#create repository.ts:8
depth 1 OrderService#audit service.ts:9
depth 2 OrderRepository#find repository.ts:9
depth 2 ext:console external boundary
depth 2 __unknown__:this.rows.push receiver type unknown
// unresolved calls stay visible instead of vanishing —
// widen the call with get_node() and try again.
Java · which override runs, and what wraps it
com.acme.Payment
IMPLEMENTS com.acme.CardPayment depth 1
IMPLEMENTS com.acme.UpiPayment depth 1
// find_routes() · find_injections()
[] no web framework, no DI container
2 overrides · every target a Payment call can reach
com.acme.AuditAspect
auditPayment around paymentCall()
ADVISES CardPayment#pay · Payment#pay · UpiPayment#pay
beforeCheckout before execution(* com.acme.Checkout.checkout(..))
ADVISES Checkout#checkout
paymentCall pointcut execution(* com.acme.*.pay(..))
named pointcut — resolves to 3 targets, advises none itself
depth 1 AuditAspect#beforeCheckout INTERCEPT · before
depth 1 Payment#pay CALLS · Checkout.java:11
depth 2 AuditAspect#auditPayment INTERCEPT · around
depth 2 CardPayment#pay DYNAMIC_DISPATCH
depth 2 UpiPayment#pay DYNAMIC_DISPATCH
depth 3 CardPayment#authorize CardPayment.java:6
depth 3 UpiPayment#settle UpiPayment.java:6
depth 3 __unknown__:joinPoint.proceed AuditAspect.java:17
depth 4 __unknown__:System.out.println UpiPayment.java:10
No web framework and no DI container in this fixture, so find_routes()
and find_injections() return [] — the honest answer
rather than a guess. What it exercises is the harder question: which override
actually runs, and which advice wraps it. The five walkthroughs below use this
fixture.
Impact analysis: who reaches this method?
Before changing Payment#pay, ask for every caller — including
virtual-dispatch and advice callers that a text search misses.
get_callers({
symbol: "com.acme.Payment#pay",
transitive: true,
include_dynamic: true,
include_advice: true
})
{
"target": {
"id": "com.acme.Payment#pay",
"kind": "method",
"filePath": "com/acme/Payment.java"
},
"callers": [
{
"caller": {
"id": "com.acme.AuditAspect#auditPayment",
"kind": "method"
},
"depth": 1,
"edgeKind": "ADVISES"
},
{
"caller": {
"id": "com.acme.Checkout#checkout",
"kind": "method"
},
"depth": 1,
"callSiteLine": 11,
"edgeKind": "CALLS"
},
{
"caller": {
"id": "com.acme.AuditAspect#beforeCheckout",
"kind": "method"
},
"depth": 2,
"edgeKind": "ADVISES"
}
]
}
Both advice callers are the point. AuditAspect#auditPayment never
mentions pay in its own body — it inherits the dependency from a
named pointcut. beforeCheckout only reaches pay at depth
2, through its advised caller. A text search finds neither.
Runtime trace: what actually executes?
trace_execution follows static calls, then fans out across virtual
dispatch and weaves advice in as INTERCEPT hops.
trace_execution({
symbol: "com.acme.Checkout#checkout",
include_dynamic: true,
include_advice: true,
max_depth: 6
})
{
"root": { "id": "com.acme.Checkout#checkout", "kind": "method" },
"stepCount": 9,
"steps": [
{
"symbol": { "id": "com.acme.AuditAspect#beforeCheckout" },
"depth": 1,
"via": "INTERCEPT"
},
{
"symbol": { "id": "com.acme.Payment#pay" },
"depth": 1,
"via": "CALLS",
"callSiteLine": 11
},
{
"symbol": { "id": "com.acme.AuditAspect#auditPayment" },
"depth": 2,
"via": "INTERCEPT"
},
{
"symbol": { "id": "com.acme.CardPayment#pay" },
"depth": 2,
"via": "DYNAMIC_DISPATCH"
},
{
"symbol": { "id": "com.acme.UpiPayment#pay" },
"depth": 2,
"via": "DYNAMIC_DISPATCH"
},
{
"symbol": { "id": "__unknown__:joinPoint.proceed" },
"depth": 3,
"via": "CALLS",
"callSiteLine": 17
},
{
"symbol": { "id": "com.acme.CardPayment#authorize" },
"depth": 3,
"via": "CALLS",
"callSiteLine": 6
},
{
"symbol": { "id": "com.acme.UpiPayment#settle" },
"depth": 3,
"via": "CALLS",
"callSiteLine": 6
},
{
"symbol": { "id": "__unknown__:System.out.println" },
"depth": 4,
"via": "CALLS",
"callSiteLine": 10
}
]
}
Reading the via field is the whole point: CALLS is
what the source says, DYNAMIC_DISPATCH is what the hierarchy
allows at runtime, and INTERCEPT is behaviour injected by an
aspect.
Aspect audit: what intercepts what?
Pointcuts are resolved — named pointcut references, execution,
wildcards, and boolean operators all produce concrete ADVISES
edges.
find_aspects({ limit: 20 })
{
"aspectCount": 1,
"aspects": [
{
"aspect": { "id": "com.acme.AuditAspect" },
"advice": [
{
"method": { "id": "com.acme.AuditAspect#auditPayment" },
"adviceKind": "around",
"pointcut": "paymentCall()",
"advised": [
{ "id": "com.acme.CardPayment#pay" },
{ "id": "com.acme.Payment#pay" },
{ "id": "com.acme.UpiPayment#pay" }
]
},
{
"method": { "id": "com.acme.AuditAspect#beforeCheckout" },
"adviceKind": "before",
"pointcut": "execution(* com.acme.Checkout.checkout(..))",
"advised": [
{ "id": "com.acme.Checkout#checkout" }
]
},
{
"method": { "id": "com.acme.AuditAspect#paymentCall" },
"adviceKind": "pointcut",
"pointcut": "execution(* com.acme.*.pay(..))",
"advised": []
}
]
}
]
}
@Around("paymentCall()") is reported with the resolved effect — all
three pay methods — not just the pointcut name. The named pointcut
itself appears with an empty advised list: it declares a predicate,
it does not intercept anything. This is the difference between indexing an
annotation and understanding it.
Decorators: runtime semantics attached to code
Annotations are graph nodes, so you can find every symbol carrying one — or list everything attached to a single type and its members.
find_annotated({
annotation: "Transactional",
kind: "method"
})
{
"annotation": "Transactional",
"count": 1,
"results": [
{
"symbol": {
"id": "com.acme.DecoratedService#describe",
"kind": "method",
"filePath": "com/acme/DecoratedService.java"
},
"annotation": "Transactional",
"line": 9,
"arguments": ["readOnly = true"]
}
]
}
get_decorators({
symbol: "com.acme.DecoratedService",
include_members: true
})
// decorators: [ { name: "Service", decorator: false } ]
// members[0].symbol.id: "com.acme.DecoratedService#describe"
// members[0].decorators:
// { name: "Cacheable", decorator: true, arguments: ["\"orders\""] }
// { name: "Transactional", decorator: true, arguments: ["readOnly = true"] }
// string literals keep their quotes — that is the source, not a JSON artifact
decorator: true marks framework annotations whose semantics are
behavioural — caching, transactions, retries, async execution, security.
Structural annotations such as @Override or @Service
are indexed but not flagged.
Hotspots and dead-code candidates
Two ends of the same graph: where the traffic concentrates, and what nothing calls.
{
"count": 5,
"hotspots": [
{
"symbol": { "id": "com.acme.Payment#pay" },
"fanIn": 1,
"fanOut": 2,
"adviceCount": 1
},
{
"symbol": { "id": "com.acme.CardPayment#authorize" },
"fanIn": 1,
"fanOut": 1,
"adviceCount": 0
},
{
"symbol": { "id": "com.acme.CardPayment#pay" },
"fanIn": 1,
"fanOut": 1,
"adviceCount": 1
},
{
"symbol": { "id": "com.acme.UpiPayment#pay" },
"fanIn": 1,
"fanOut": 1,
"adviceCount": 1
},
{
"symbol": { "id": "com.acme.UpiPayment#settle" },
"fanIn": 1,
"fanOut": 1,
"adviceCount": 0
}
]
}
{
"count": 0,
"unused": []
}
That empty result is correct, not a bug: the fixture lives under
tests/, and test sources are excluded by default. Re-running with
include_tests: true and include_constructors: true
surfaces five candidates — including AuditAspect#auditPayment,
which is reached by the framework, not by any call. ADVISES edges
point from advice to the advised method, so nothing points at
the advice itself. Treat every row as a candidate, never a verdict.
Composing the primitives
Real questions combine tools. “Is this aspect safe to remove?” becomes
find_aspects → get_callers on each advised method →
trace_path from the application entry point. “What breaks if I change
this interface?” becomes find_implementations →
get_callers per override → find_annotated to spot
transactional or cached implementations that carry extra contracts.