Prev Next

AI / AST driven Knowledge Graph architecture Interview questions

Last updated

1. What is an Abstract Syntax Tree (AST)? 2. What is a knowledge graph? 3. What is an AST-driven knowledge graph? 4. What are the typical node types in a code knowledge graph? 5. What are the typical edge types in a code knowledge graph? 6. What is tree-sitter and why is it used for AST extraction? 7. What is a property graph? 8. What is the purpose of a symbol table in graph construction? 9. List the main stages of an AST-to-graph ingestion pipeline? 10. What is a call graph? 11. What is the purpose of a graph schema in code knowledge graphs? 12. What is Cypher? 13. What is a Code Property Graph (CPG)? 14. What is name resolution in code analysis? 15. What is the purpose of stable node identifiers? 16. What is GraphRAG in the context of code? 17. What are LSIF and SCIP? 18. How do you use the visitor pattern to extract AST nodes? 19. What is the difference between an AST and a CST? 20. Why do we use an AST instead of regex to extract code relationships? 21. How does incremental parsing keep the graph fresh? 22. What is the difference between an RDF triple store and a property graph for code? 23. How do you model inheritance and interface implementation in the graph? 24. How is cross-file import resolution handled? 25. Why do we use content hashes to skip unchanged files? 26. What is the difference between AST, CFG and PDG? 27. When would you choose Neo4j over an in-memory graph like NetworkX? 28. How do you support multiple languages in one graph schema? 29. How do you attach embeddings to AST-derived nodes? 30. Why is chunking by AST node better than fixed-size chunking for RAG? 31. How do you handle dynamic dispatch in call edges? 32. What happens when a file is renamed or moved? 33. How does the ingestion pipeline handle syntax errors in source files? 34. How do you query the blast radius of a change? 35. How do you exclude generated code and vendored dependencies? 36. Explain the execution flow of an incremental graph update on a git commit? 37. Explain the internal working of tree-sitter incremental parsing? 38. How can you optimize bulk graph ingestion into Neo4j? 39. How do you resolve cross-language edges in a code knowledge graph? 40. How do you version the graph across commits and branches? 41. Which is better for code retrieval: graph traversal, vector search or a hybrid, and why? 42. How do you troubleshoot missing call edges? 43. How do you design the graph for monorepos with millions of nodes? 44. How do you prevent secrets and proprietary code leaking through the graph? 45. Explain how an LLM coding agent uses the graph to complete a change? 46. How do you validate graph correctness against the compiler or LSP? 47. What is the difference between syntactic and semantic edges, and why does it matter? 48. How does taint analysis use a code property graph? 49. How do you handle macros, templates and code generation in the graph? 50. How can you optimize graph queries for deep traversals?

1. What is an Abstract Syntax Tree (AST)?

An Abstract Syntax Tree is a tree that captures the grammatical structure of source code. Each node is a language construct such as a function definition, a call or a binary expression, and details like punctuation and whitespace are dropped.

Compilers build it right after parsing. Linters, formatters, refactoring tools and code-graph builders read it instead of raw text. For total = price * qty the tree looks like this:

Assign
 +-- target: Name(total)
 +-- value:  BinOp(*)
        +-- Name(price)
        +-- Name(qty)

Because every node knows its type, its children and its position in the file, a program can ask structural questions such as "which functions call open()" without guessing at text patterns.

Take quiz
What does an AST deliberately leave out compared with the raw source?
Variable names and function names
The order of statements
Punctuation and whitespace that carry no structural meaning
In 'total = price * qty', which node is the direct parent of Name(price)?
The multiplication (BinOp) node
The Assign node
The Name(total) node

2. What is a knowledge graph?

A knowledge graph stores information as entities (nodes) joined by typed relationships (edges), usually under a schema or ontology that says which entity and relationship types are allowed.

The value sits in the connections. Questions that span several hops, such as "which services depend on a library that owns this table", become a traversal instead of a chain of joins.

A single fact is a triple like (Alice)-[:WORKS_ON]->(PaymentService). The two common storage models are the property graph (Neo4j, Memgraph) and RDF triples (queried with SPARQL). In software engineering the entities are files, classes, functions and teams, and the edges are things like imports, calls and ownership.

Take quiz
What makes a knowledge graph different from a plain table of entities?
Relationships are first-class, typed and traversable
It can only store free-text documents
Every entity must carry a vector embedding
In (Alice)-[:WORKS_ON]->(PaymentService), what is WORKS_ON?
A node label
A typed relationship between two entities
A property stored on PaymentService

3. What is an AST-driven knowledge graph?

An AST-driven knowledge graph is a graph whose nodes and edges are produced mechanically by parsing source code into ASTs and extracting declarations and relationships: files, classes, functions, imports, calls and inheritance. Nothing is typed in by hand and nothing is guessed by a language model.

That makes it deterministic: the same source and the same parser version give the same graph, and every node carries an exact file path and line range. It also makes incremental updates practical, because a changed file can simply be re-parsed.

Usually only declaration-level facts are kept, since storing every expression node would swamp the database. Typical uses are code search, impact analysis, onboarding maps and giving LLM agents a precise view of a repository.

Take quiz
What supplies the entities and edges in an AST-driven knowledge graph?
LLM guesses based on README text
Deterministic extraction from parsed syntax trees
Runtime profiler traces from production
Why is an AST-driven graph reproducible?
Every commit is stored forever
Embeddings are always identical between runs
The same source and parser version always yield the same nodes and edges

4. What are the typical node types in a code knowledge graph?

Most code graphs share a small core of labels: Repository, Package or Module, File, Class (or Interface/Struct), Function (or Method), Variable/Field/Parameter and Import. Call sites are modelled either as their own nodes or as properties on CALLS edges, and comments or commits are common optional extras.

Node label AST construct (Python example) Useful properties
File module (root node) path, language, content_hash
Class class_definition qualified_name, start_line, end_line
Function function_definition name, signature, start_line, end_line
Import import_statement, import_from_statement target_module, alias

Keep the label set small and language-neutral. Adding a label for every grammar rule turns the graph into the AST itself, which is exactly what you are trying to avoid.

Take quiz
Which AST construct typically becomes a Function node in a Python graph?
import_from_statement
string
function_definition
Which File property is most useful for skipping unchanged files on re-index?
content_hash
line_count
language

5. What are the typical edge types in a code knowledge graph?

The usual relationship types are CONTAINS (or DEFINES), IMPORTS, CALLS, EXTENDS, IMPLEMENTS, OVERRIDES and REFERENCES. Decide the direction once and document it: (a)-[:CALLS]->(b) always means a calls b.

Edge From -> To Derived from
CONTAINS File -> Class -> Method tree nesting
IMPORTS File -> Module import statements plus resolution
CALLS Function -> Function call expressions plus resolution
EXTENDS / IMPLEMENTS Class -> Class or Interface base-class lists
OVERRIDES Method -> Method matching signatures along the hierarchy

Edge properties such as line or confidence are worth adding early, because retrofitting them later means re-indexing.

Take quiz
In (a)-[:CALLS]->(b), what does the arrow direction mean?
a calls b
b calls a
a and b live in the same file
Which edge type comes purely from tree nesting?
CALLS
CONTAINS
OVERRIDES

6. What is tree-sitter and why is it used for AST extraction?

Tree-sitter is an incremental parsing library written in C, with ready-made grammars for dozens of languages and bindings for Python, JavaScript, Rust, Go and more. It is popular for graph building because one API covers many languages.

  • Fast and incremental: after an edit it reuses unchanged subtrees.
  • Error tolerant: half-typed or broken files still produce a tree.
  • Query language: S-expression patterns capture nodes without hand-written walkers.
from tree_sitter import Language, Parser
import tree_sitter_python as tspython

parser = Parser(Language(tspython.language()))
tree = parser.parse(b"def add(a, b): return a + b")
print(tree.root_node.type)   # module

The catch is that it yields a syntax tree only. It knows nothing about types or which declaration a name refers to, so cross-file resolution is your job.

Take quiz
Which tree-sitter property helps when a file is half-typed?
It automatically fixes the syntax errors in the file
Error-tolerant parsing that still returns a tree
It refuses to parse files with errors
What does tree-sitter not provide on its own?
A syntax tree for the source text
Incremental re-parsing after an edit
Type information and cross-file symbol resolution

7. What is a property graph?

A property graph is a data model where nodes carry one or more labels and key-value properties, and relationships are directed, typed and can carry properties too. Neo4j, Memgraph and other openCypher databases use it.

For code, that means a call can store its own details instead of needing a helper node:

CREATE (a:Function {name: 'checkout', file: 'cart.py', start_line: 12})
CREATE (b:Function {name: 'charge',   file: 'billing.py', start_line: 40})
CREATE (a)-[:CALLS {line: 27, confidence: 1.0}]->(b)

Here line and confidence live on the CALLS relationship. In RDF you would need reification or RDF-star to say the same thing.

Take quiz
Where would you store the line number of a call in a property graph?
In a separate lookup table only
Inside the node label
As a property on the CALLS relationship
Which statement about relationships in a property graph is true?
They are directed, typed and can have properties
They are untyped and cannot carry properties
They must connect nodes with the same label

8. What is the purpose of a symbol table in graph construction?

A symbol table maps names to declarations within scopes. The AST only says that a call to process(order) exists. The symbol table says which process it is.

Construction usually takes two passes. Pass one walks every file and records declarations. Pass two walks references and looks each name up through the scope chain (block, function, class, module, builtins), which is when CALLS and REFERENCES edges get created.

Without it you fall back to name matching, and two unrelated functions called process in different modules end up connected to every caller of either one.

Take quiz
What does a symbol table let the builder do?
Resolve a name at a call site to its declaration
Compress the AST for storage
Generate embeddings for functions
Why are two passes commonly used?
To parse each file twice for speed
All declarations must be known before references can be resolved
To strip comments before parsing

9. List the main stages of an AST-to-graph ingestion pipeline?

A typical pipeline has seven stages, and each can be tested on its own:

  1. Discover files (git ls-files, ignore rules, language detection).
  2. Parse each file into an AST.
  3. Extract entities and raw references with queries or visitors.
  4. Resolve names across files into concrete targets.
  5. Normalize into the schema and assign stable IDs.
  6. Load with batched upserts.
  7. Enrich with embeddings, metrics or ownership data.

Keeping the stages separate means a bug in resolution can be fixed and re-run from cached ASTs without re-parsing the whole repository.

flowchart LR
  A[Discover] --> B[Parse] --> C[Extract] --> D[Resolve]
  D --> E[Normalize] --> F[Load] --> G[Enrich]
Take quiz
Which stage can only run after every file has been parsed?
File discovery
Cross-file symbol resolution
Single-file parsing
What is the job of the discovery stage?
Create CALLS edges
Generate embeddings
Choose which files to ingest using ignore rules and language detection

10. What is a call graph?

A call graph is a directed graph in which nodes are functions and an edge from A to B means A may invoke B. A static call graph is built by finding call expressions inside each function body and resolving the callee.

def checkout(cart):
    total = price(cart)
    charge(total)

# edges: checkout -> price, checkout -> charge

The word may matters. Callbacks and dynamic dispatch can hide the real target. Call graphs power impact analysis, dead-code detection (no incoming edges, excluding entry points) and reachability checks from main or a request handler.

Take quiz
A function with no incoming CALLS edges is:
Guaranteed to be unused
Always recursive
A dead-code candidate or an entry point such as main or a handler
Why is a static call graph edge described as 'may call'?
Dynamic dispatch and callbacks can make the real target unknowable statically
The parser skips some calls at random
Edges are deleted after each build

11. What is the purpose of a graph schema in code knowledge graphs?

A graph schema fixes the vocabulary: allowed labels, relationship types, required properties and constraints. Without one, different extractors drift. One emits CALLS, another INVOKES, a third calls, and every query has to know all three.

Enforcing it in the database catches mistakes early:

CREATE CONSTRAINT function_id IF NOT EXISTS
FOR (f:Function) REQUIRE f.id IS UNIQUE;

The schema also helps LLM agents. Giving the model the real labels and relationship types lets it write valid Cypher instead of inventing names.

Take quiz
What problem does a fixed schema prevent?
Different extractors emitting inconsistent names like CALLS and INVOKES
Slow parsing of large files
Merge conflicts in git
Why give the schema to an LLM that writes Cypher?
It can bypass authentication
It can use real labels and relationship types instead of inventing them
It can rewrite the language grammar

12. What is Cypher?

Cypher is a declarative query language for property graphs, created at Neo4j and carried forward by the openCypher project. Patterns are drawn in ASCII art: parentheses for nodes, square brackets for relationships. It also influenced the ISO GQL standard.

MATCH (caller:Function)-[:CALLS]->(f:Function {name: 'charge'})
RETURN caller.name, caller.file

Adding a length range turns it into a traversal. -[:CALLS*1..3]-> follows between one and three CALLS hops, which is the basis of most impact-analysis queries.

Use query parameters such as $name instead of string concatenation. It keeps queries reusable, lets the database cache plans, and removes any risk of injection when an agent supplies the value.

Take quiz
In MATCH (a)-[:CALLS]->(b), what is matched?
All nodes named a and b regardless of links
Pairs of nodes where a has a CALLS relationship pointing at b
Only b nodes with no outgoing edges
What does [:CALLS*1..3] express?
Exactly three parallel CALLS relationships
At most one CALLS relationship per node
Paths of one to three CALLS hops

13. What is a Code Property Graph (CPG)?

A Code Property Graph merges three classic program representations into one property graph: the AST, the control flow graph (CFG) and the program dependence graph (PDG). The idea comes from Yamaguchi et al. (IEEE S&P 2014) and is implemented by the open-source Joern tool.

Layer Question it answers
AST What is this code made of?
CFG In what order could statements execute?
PDG Which statements depend on which values?

Because the layers share nodes, a single query can mix syntax, execution order and data flow. That is why CPGs are popular for vulnerability hunting.

Take quiz
Which three representations does a CPG combine?
AST, UML diagrams and ER diagrams
Lexer tokens, bytecode and heap dumps
AST, control flow graph and program dependence graph
Why is a CPG useful for vulnerability queries?
One query can combine structure, execution order and data dependence
It stores only function names so scans are tiny
It removes the need for source code

14. What is name resolution in code analysis?

Name resolution binds every identifier use to its declaration by following the language's scoping rules. It handles shadowing, imports and aliases, qualified names and members inherited from parent classes. The results feed REFERENCES and CALLS edges.

from billing import charge as pay
pay(order)   # resolves to billing.charge, not to any local "pay"

Textual matching would look for a function called pay and fail. Resolution tracks the alias through the import, which is exactly what a graph needs to connect the call to the right charge.

Take quiz
After 'from billing import charge as pay', a call pay(order) should resolve to:
billing.charge
Any function named pay anywhere in the repo
An unresolved external symbol
What does shadowing mean in name resolution?
A deleted variable remains usable
An inner-scope name hides an outer-scope name
Two files share one namespace

15. What is the purpose of stable node identifiers?

A stable identifier gives the same entity the same ID on every re-index. That lets you MERGE instead of creating duplicates, keeps annotations and edges attached, and makes diffing between commits possible.

A good ID is built from facts about the entity, for example repo + path + qualified_name + signature, usually hashed. Three things to avoid:

  • Auto-increment database IDs, which change when the graph is rebuilt.
  • Line numbers, which shift whenever code above the entity changes.
  • Random UUIDs per run, which defeat upserts entirely.
Take quiz
Why are line numbers a poor ID component?
They are too long to index
They shift whenever code above the entity changes
They differ between operating systems
What does a stable ID make possible during re-indexing?
Skipping the parsing step
Removing the need for a schema
Upserting nodes without creating duplicates

16. What is GraphRAG in the context of code?

In a code setting, GraphRAG means retrieval-augmented generation where retrieval walks the graph. A search finds seed nodes, then the system expands along edges (definitions, callers, callees, imports) and hands the connected context to the LLM.

Plain vector RAG returns the top-k most similar chunks and misses code that is related by structure rather than wording. The graph supplies that missing link, such as the helper a function calls or the interface it implements.

Cap the expansion at one or two hops and rank before prompting. Unbounded expansion floods the context window with loosely related code.

Take quiz
What does graph expansion add over top-k vector retrieval?
A larger embedding dimension
Automatic test generation
Structurally related code such as callers, callees and definitions
Why cap the expansion depth?
Unbounded hops pull in loosely related code and flood the context window
Graph databases cannot traverse more than one hop
Embeddings exist only for the first hop

17. What are LSIF and SCIP?

Both are formats for precomputed code intelligence: definitions, references and hover data. LSIF (Language Server Index Format) is a JSON graph of vertices and edges derived from Language Server Protocol concepts. SCIP is Sourcegraph's successor, serialized with Protocol Buffers and using readable string symbol IDs instead of a numeric graph, which makes indexes smaller and faster to process.

They matter here because the indexers behind them (scip-java, scip-typescript and others) run real compilers or type checkers. Importing a SCIP index gives your graph compiler-accurate cross-file edges without writing a resolver for each language.

Take quiz
What advantage does a SCIP index offer an AST-driven graph builder?
Compiler-accurate definitions and references without writing a resolver
It replaces the need for a graph database
It stores production performance data
Which serialization format does SCIP use?
XML
Protocol Buffers
CSV

18. How do you use the visitor pattern to extract AST nodes?

A visitor walks the tree and dispatches to a method per node type, so extraction logic sits in small visit_* methods. Python's standard ast module shows it well:

import ast

class Extractor(ast.NodeVisitor):
    def __init__(self):
        self.functions, self.calls, self.stack = [], [], []

    def visit_FunctionDef(self, node):
        self.functions.append(node.name)
        self.stack.append(node.name)
        self.generic_visit(node)        # keep walking the body
        self.stack.pop()

    def visit_Call(self, node):
        if isinstance(node.func, ast.Name) and self.stack:
            self.calls.append((self.stack[-1], node.func.id))
        self.generic_visit(node)

ex = Extractor()
ex.visit(ast.parse(open("app.py").read()))

The stack records the enclosing function so each call gets a caller. generic_visit is what makes the walk continue into children. This sketch ignores async functions and attribute calls such as obj.run(), which need their own handlers.

Take quiz
Why does visit_FunctionDef call generic_visit?
To close the file handle
To continue descending into the function body
To rename the function
What is the stack used for in the extractor?
Sorting functions alphabetically
Caching parsed trees
Knowing which function encloses each call

19. What is the difference between an AST and a CST?

A concrete syntax tree (parse tree) keeps every token the grammar matched, including parentheses, commas and keywords. An AST keeps only the structure that carries meaning.

CST (parse tree) AST
Contains every token, punctuation included Contains only meaningful constructs
Larger and tied closely to the grammar Smaller and closer to language semantics
Good for formatters and style-preserving refactoring Good for analysis, compilers and graph building
Tree-sitter produces this Python's ast and Roslyn expose this style

Tree-sitter returns a CST with named and anonymous nodes. Filtering on is_named removes the punctuation and gives you an AST-like view.

For graph building that choice rarely matters much. What matters is deciding once which view your extractors expect, so queries and node-type names stay consistent across languages.

Take quiz
Which tree suits a formatter that must preserve every token?
The AST
A call graph
The CST
In tree-sitter, how do you skip punctuation tokens?
Keep only named nodes (is_named)
Delete every leaf node
Call tree.simplify()

20. Why do we use an AST instead of regex to extract code relationships?

Regex works on characters. Code is a nested grammar, so regex cannot tell a real call from text that merely looks like one, and it cannot match arbitrarily nested parentheses at all.

# TODO: remove charge(order)
msg = "call charge(order) later"
charge(order)

A search for charge\( reports three hits. An AST query reports one, because the first two are a comment and a string node. Multi-line calls, decorators, nested calls and scoping rules cause the same trouble.

An AST also gives exact start and end positions per node, which regex only approximates. For a graph that people and agents will trust, that precision is the whole point.

Take quiz
Why does a regex for 'charge(' match inside a string literal?
It works on characters and has no notion of syntax nodes
Strings are always parsed as function calls
Regex engines ignore quotes by design
Why can regex not reliably match arbitrarily nested parentheses?
Parentheses are invalid regex characters
Nested structure is beyond what regular expressions can express
Regex cannot read multi-line files

21. How does incremental parsing keep the graph fresh?

With tree-sitter, you tell the old tree about the edit using tree.edit(...) and then re-parse, passing that tree as the starting point. Subtrees outside the edited region are reused, so cost tracks the size of the edit rather than the size of the file.

After the new parse, old_tree.changed_ranges(new_tree) reports which regions changed structurally. Map those ranges to enclosing function or class nodes and you know exactly which graph nodes to update.

old_tree.edit(start_byte=120, old_end_byte=130, new_end_byte=142,
              start_point=(8, 4), old_end_point=(8, 14), new_end_point=(8, 26))
new_tree = parser.parse(new_source, old_tree)
for r in old_tree.changed_ranges(new_tree):
    print(r.start_point, r.end_point)

This pays off in live agent or editor integrations. For commit-triggered indexing, re-parsing whole changed files is usually fast enough.

Take quiz
What does changed_ranges tell you?
Which commits touched the file
Which regions of the tree differ structurally between old and new parse
Which functions lack tests
What must you do before re-parsing with the old tree?
Delete the old tree
Serialize the tree to JSON
Apply tree.edit() to describe the text change

22. What is the difference between an RDF triple store and a property graph for code?

Both can hold a code graph. They differ in how facts are shaped and what you get for free.

Aspect RDF triple store Property graph
Unit of data Subject-predicate-object triple Node and relationship, each with properties
Edge attributes Reification or RDF-star Native properties on relationships
Query language SPARQL Cypher or Gremlin
Reasoning RDFS and OWL inference built in Usually done in application code
Identifiers IRIs, designed for linked data Internal IDs plus your own keys

Pick RDF when you need ontology reasoning or want to link with outside vocabularies. Pick a property graph when the workload is traversal-heavy developer tooling, such as callers, imports and impact analysis, and you want line numbers and confidence on edges without ceremony.

Take quiz
Which model makes it easiest to attach a line number to a CALLS edge?
RDF, because triples have a fifth slot for properties
Neither, edges can never hold data
Property graph, because relationships carry properties natively
Which query language is used with RDF stores?
SPARQL
Cypher
Gremlin

23. How do you model inheritance and interface implementation in the graph?

Use EXTENDS between Class nodes, IMPLEMENTS from a class to an interface, and OVERRIDES from a method to the parent method it replaces.

  1. Read base-class names from the AST (for Python, the argument list of class_definition; for Java, superclass and super_interfaces).
  2. Resolve each name through imports to a Class node.
  3. Create the edge. If the base is external, point to a stub node instead of dropping it.
  4. Walk each hierarchy and match methods by name and arity or signature to create OVERRIDES.
MATCH (c:Class)-[:IMPLEMENTS|EXTENDS*1..5]->(i:Interface {name: 'PaymentGateway'})
RETURN c.qualified_name

That query lists every implementation of an interface, direct or inherited, which dispatch-aware call resolution also depends on.

Take quiz
Which edge links a subclass method to the parent method it replaces?
OVERRIDES
IMPLEMENTS
CONTAINS
What should an unresolved external base class become?
A dropped edge
An external stub node so the EXTENDS edge is not lost
A duplicate of the subclass

24. How is cross-file import resolution handled?

It is a two-pass job. First build a module index that maps module paths to File nodes and records each file's exports. Then resolve each import against that index and store the result as an IMPORTS edge plus a binding (alias to target) that name resolution reuses.

Rules differ by language. Python uses package directories and __init__.py. JavaScript and TypeScript use relative paths, extension and index lookups, tsconfig path aliases and node_modules. Java maps packages to directories and the classpath.

The awkward cases are re-exports (export * from), barrel files, circular imports and dynamic imports. Packages outside the repository resolve to stub Package nodes, so edges into libraries still land somewhere.

Take quiz
Why build a module index before resolving imports?
Imports are always resolved in file order
An import can point to a file that has not been parsed yet
It reduces disk space
What is a barrel file in JavaScript or TypeScript?
A file containing only comments
A compiled bundle of dependencies
A file that re-exports symbols from other modules

25. Why do we use content hashes to skip unchanged files?

Store a hash (SHA-256 of the file bytes) on each File node. On re-index, compute the hash first; if it matches, skip parsing and keep the existing nodes. Modification times are unreliable because fresh clones and CI checkouts reset them.

Hashes bring extra wins. Identical content at two paths (vendored copies, for example) can share cached parse results, and per-function body hashes tell you which declarations really changed, so embeddings are regenerated only for those.

One caveat: an unchanged file can still have stale edges. If a function it calls was renamed in another file, the hash is the same but the resolved CALLS edge is wrong. Dependents must be re-resolved even when they are not re-parsed.

Take quiz
Why is a content hash more reliable than modification time?
Hashes encrypt the source code
mtime cannot be read on Linux
Fresh clones reset timestamps, but identical content gives an identical hash
A file's hash is unchanged but a file it calls into changed. What may need updating?
The file's outgoing resolved edges
Nothing, its edges are always valid
The file's own hash

26. What is the difference between AST, CFG and PDG?

They describe the same program from three angles.

Graph Nodes Edges mean Answers
AST Syntactic constructs Parent-child nesting What is this code made of?
CFG Statements or basic blocks Possible execution order Can B run after A?
PDG Statements Control and data dependence Does this value affect that statement?

Take an if/else. The AST shows an if node with two branches. The CFG shows a split into both branches and a merge afterwards. The PDG shows that a later statement uses a variable assigned in one branch.

A call graph and an import graph are built on top of the AST. CFG and PDG need extra analysis, which is why plain AST-driven graphs usually stop at declarations and calls.

Take quiz
Which graph shows possible execution order between statements?
The CFG
The AST
The import graph
Which graph answers whether an assignment influences a later SQL call?
The AST
The PDG, through data dependence
The directory tree

27. When would you choose Neo4j over an in-memory graph like NetworkX?

NetworkX and igraph are excellent for exploration. A database becomes the right choice once the graph must be shared, durable or updated incrementally.

Concern In-memory (NetworkX) Neo4j
Scale Bound by process RAM Disk-backed, much larger graphs
Persistence Rebuild or pickle Durable with transactions
Concurrency One process Many clients and agents at once
Querying Python code Cypher with indexes
Algorithms Rich libraries Graph Data Science library

Use NetworkX for notebooks and one-off analysis of a single repository. Move to Neo4j when several users or agents query it, when it must survive restarts, or when commits trigger incremental updates.

A common path is to prototype in NetworkX, settle the schema there, and then port the loader to Neo4j unchanged in shape.

Take quiz
Which scenario favours an in-memory NetworkX graph?
A shared service queried by many agents
A one-off notebook analysis of a single repository
A graph needing transactional updates that survive restarts
What is a limit of NetworkX on very large code bases?
It cannot represent directed edges
It cannot store node attributes
The whole graph must fit in process memory

28. How do you support multiple languages in one graph schema?

Keep the labels language-neutral (File, Class, Function, Import, Call), store a language property, and move the differences into per-language extraction config. Tree-sitter queries make this easy because each language gets its own query file mapping its node types onto the shared labels.

Concept Python Java Go
Function function_definition method_declaration function_declaration, method_declaration
Class-like class_definition class_declaration type_declaration (struct)
Import import_statement import_declaration import_declaration
Call call method_invocation call_expression

Language-specific ideas such as traits or structs can be added as secondary labels. Adding a language then means writing a query file and a resolver, not changing the schema or existing Cypher.

Keep a small conformance test per language: a fixture file with a known number of functions, classes and calls, asserted after extraction.

Take quiz
How do you keep the schema stable when adding Go support?
Create a separate database per language
Rewrite all existing Cypher queries
Map Go node types onto the existing generic labels with per-language queries
Which tree-sitter node type represents a Java method call?
method_invocation
function_definition
import_declaration

29. How do you attach embeddings to AST-derived nodes?

Embed declarations, not whole files. For each Function or Class node, build a text such as path | class | signature | docstring | body and embed that. The header gives the model context the body alone lacks.

Store the vector as a node property with a vector index (Neo4j 5.x supports this), or keep vectors in an external store keyed by node ID. Record the content hash used to build the embedding.

Re-embed only when a node's hash changes. Search then runs in two steps: the vector index returns seed node IDs, and a graph query expands from them to callers, callees and definitions.

Take quiz
When should a node's embedding be regenerated?
When the node's content hash changes
On every query
Only when the repository is renamed
Why prefix the embedded text with path and signature?
It makes vectors smaller
It gives the embedding model context that the body alone lacks
It guarantees exact keyword matches

30. Why is chunking by AST node better than fixed-size chunking for RAG?

Fixed-size chunking cuts wherever the token budget ends: mid-function, between a signature and its body, even inside a string. The embedding of such a chunk describes half an idea.

AST-aware chunking splits on syntactic boundaries. A function becomes one chunk. An oversized function is split at inner statement blocks. Small neighbours are merged up to the budget. Each chunk also carries parent context such as the class name and relevant imports.

Fixed-size AST-based
Boundaries ignore syntax Boundaries follow functions and classes
Little metadata Qualified name, kind and line range
No link to the graph Each chunk maps to a graph node

That last row is what lets retrieval jump from a matching chunk to its callers and callees.

Take quiz
What happens to a 3,000-token function under AST chunking?
It is truncated at the limit
It is split at inner statement blocks, keeping each chunk syntactically meaningful
It is dropped from the index
What extra benefit do AST chunks give?
They need no embeddings
They are always exactly equal in size
Each chunk maps to a graph node you can expand from

31. How do you handle dynamic dispatch in call edges?

A static analysis cannot always know the runtime type of obj.run(). Pick a strategy by how much precision you can afford:

Strategy Candidates for obj.run() Trade-off
Name matching Every method called run High recall, many false edges
Class Hierarchy Analysis run in the declared type and its subtypes Better precision, cheap
Rapid Type Analysis Same, limited to classes that are instantiated Tighter, needs whole program
Points-to analysis Types that can actually flow to the receiver Most precise, expensive
Compiler or SCIP data Whatever the type checker resolved Accurate, needs a build

Whatever you choose, record it. Put resolution and confidence on the CALLS edge so consumers can ask for exact edges only. When nothing resolves, point to an unresolved placeholder rather than silently dropping the call.

Take quiz
Which strategy limits candidates to subtypes of the receiver's declared type?
Name-only matching
Dead-code elimination
Class Hierarchy Analysis
Why store a resolution or confidence property on CALLS edges?
So consumers can separate exact edges from heuristic guesses
It speeds up graph writes
The database requires it

32. What happens when a file is renamed or moved?

If node IDs include the file path, a rename looks like a delete plus an add. Annotations are lost and incoming edges dangle until they are re-resolved.

  1. Detect renames from git with git diff -M (similarity-based) or by matching content hashes.
  2. Remap old IDs to new IDs, or build IDs from qualified name instead of path.
  3. Update the path property on the File node and its children.
  4. Re-resolve importers, because their import targets point at the old location.

Note that qualified names can change on a move too (a Python module path follows its directory), so a pure qualified-name ID is not a complete fix. Rename detection plus dependent re-resolution is the reliable combination.

Take quiz
Which git feature helps detect a rename during incremental indexing?
git diff -M (rename detection)
git stash
git bisect
Which files need re-resolution after a module is moved?
Only the moved file
Files that import the moved module
None, edges repair themselves

33. How does the ingestion pipeline handle syntax errors in source files?

Tree-sitter never refuses a file. It inserts ERROR or MISSING nodes where it cannot parse, and root_node.has_error tells you it happened. What you do next is a policy decision:

  1. Keep the last good version of that file's subgraph and flag the file with parse_status = 'error'. This is safest for live, mid-edit indexing.
  2. Partial extraction: ingest declarations whose subtrees contain no ERROR nodes.
  3. Fail the build for committed code on protected branches, where a syntax error is a real defect.

Compiler-based parsers behave differently. javac may abort, while Roslyn and JavaParser can return partial trees. Track the error rate per language as a pipeline metric, because a sudden jump usually means a grammar or version mismatch rather than bad code.

Take quiz
What does tree-sitter place in the tree where it cannot parse?
A null root
ERROR or MISSING nodes
An exception that halts parsing of all files
What is the safest policy for a half-typed file in a live graph?
Delete the file's nodes immediately
Insert guessed nodes to complete the code
Keep the last good version of that file's nodes and flag the file

34. How do you query the blast radius of a change?

Blast radius is everything that could be affected if a function changes: its transitive callers, overriding or implementing classes, importers and the tests that reach it. Traverse CALLS backwards from the changed node.

MATCH (f:Function {qualified_name: $fn})
MATCH p = (caller:Function)-[:CALLS*1..4]->(f)
RETURN DISTINCT caller.qualified_name, caller.file, length(p) AS hops
ORDER BY hops

Add WHERE caller.file STARTS WITH 'tests/' to list affected tests, or group by module or owner to see who needs a review. Bound the depth, and filter on edge confidence if you tag it.

Treat the result as a lower bound. Reflection and unresolved dynamic dispatch can create real calls that have no edge.

Take quiz
Which direction do you traverse CALLS to find who is affected by changing f?
Forward, to what f calls
Neither, use CONTAINS
Backward, from f to its callers
Why treat a blast-radius result as a lower bound?
Reflection and dynamic dispatch can create real calls with no edge
Test files are excluded automatically
Variable-length paths stop after two hops

35. How do you exclude generated code and vendored dependencies?

Generated and vendored code adds noise, inflates the graph and belongs to someone else. Filter in layers:

  • .gitignore plus a custom ignore list: node_modules, vendor, dist, build, target.
  • .gitattributes entries marked linguist-generated.
  • Header markers, such as Go's "Code generated ... DO NOT EDIT." line.
  • Heuristics for minified JavaScript, such as very long lines.

Do not throw the dependencies away completely. Represent external libraries as stub Package nodes so calls into them still resolve to something. If generated code matters (protobuf stubs, ORM models), ingest it with generated: true and link it back with a GENERATED_FROM edge to the source schema.

Take quiz
How can you detect a generated Go file?
A header comment saying 'Code generated ... DO NOT EDIT.'
A .gen file extension only
A file size over 1 MB
Why keep stub nodes for excluded dependencies?
To store license text
Calls into libraries still resolve to a node without parsing library source
To speed up git clone

36. Explain the execution flow of an incremental graph update on a git commit?

An incremental update turns a commit into the smallest set of graph changes that keeps the graph correct. The flow:

  1. Receive the commit event (webhook or CI step) with the old and new SHA.
  2. Run git diff --name-status -M old new to get added, modified, deleted and renamed files.
  3. Compute content hashes and drop files whose hash is unchanged.
  4. Parse the remaining files in parallel and extract nodes and edges per file.
  5. Diff each file's new subgraph against the stored one by stable ID: delete what vanished, upsert the rest.
  6. Find dependents by querying reverse IMPORTS and CALLS edges into changed symbols, and re-resolve only those edges.
  7. Write in batched transactions, then store the indexed_commit marker.
  8. Re-embed nodes whose hash changed and run count checks.
flowchart TD
  A["Commit event"] --> B["git diff -M"]
  B --> C{Hash changed?}
  C -- no --> Z["Skip file"]
  C -- yes --> D["Parse and extract"]
  D --> E["Diff by stable ID"]
  E --> F["Re-resolve dependents"]
  F --> G["Batched upsert"]
  G --> H["Update indexed_commit"]
  H --> I["Re-embed changed nodes"]

Make every step idempotent. A replayed webhook or a crash halfway through should leave the graph correct after a retry, never duplicated or half-updated. Writing the commit marker last is what makes resuming safe.

Take quiz
After updating changed files, which additional files need edge re-resolution?
Every file in the repository
Dependents whose references pointed at changed symbols
Only files that were deleted
Why must the update pipeline be idempotent?
So no file is ever parsed twice
So commits can be applied in any order without checks
A retried or replayed commit event must not create duplicates or corrupt edges

37. Explain the internal working of tree-sitter incremental parsing?

Tree-sitter compiles a grammar written in a JavaScript DSL (grammar.js) into a C parser. The parser is table-driven and LR-based with GLR-style handling of ambiguity, so one grammar can express constructs that a strict LR parser cannot.

Incremental behaviour rests on three ideas:

  1. Edit marking. tree.edit() records the changed byte and point range, and nodes overlapping it are flagged as needing a fresh look.
  2. Subtree reuse. While re-parsing, the parser walks the old tree and, where a subtree lies outside the edit and its surrounding context is unaffected, shifts the entire subtree in one step instead of re-lexing its tokens.
  3. Immutable, shared trees. Trees are persistent data structures. The new tree shares untouched nodes with the old one, so copies are cheap and old trees stay valid.

When it meets text it cannot parse, error recovery tries skipping or inserting tokens and picks the cheapest repair, which is where ERROR and MISSING nodes come from.

The result is that re-parse time scales with the size of the edit and its surrounding context, not with the size of the file. That is why it is practical for editor-speed updates.

Take quiz
Why is reparsing after a small edit fast?
The parser skips everything after the edit
The source is compiled to bytecode first
Unchanged subtrees from the old tree are reused as whole units
What lets old and new trees share nodes cheaply?
Trees are immutable with structural sharing
Both live in one mutable array
Nodes are copied into a database

38. How can you optimize bulk graph ingestion into Neo4j?

The bottleneck is usually writing, not parsing. Work through these in order:

  1. Create uniqueness constraints first. MERGE uses the backing index to find existing nodes; without it every lookup scans the label.
  2. Send data in batches with UNWIND (a few thousand rows per transaction), never one query per node.
  3. Load all nodes first, then relationships, matching endpoints through the indexed ID.
  4. On an empty database with guaranteed-unique IDs, use CREATE instead of MERGE.
  5. Parse in parallel worker processes but write from one or few writers. Avoid many writers touching the same hub node (such as a shared External node), which causes lock contention and deadlocks.
  6. For the very first load of a huge graph, generate CSVs and use neo4j-admin database import, which is far faster than transactional writes.
  7. Size the heap and page cache for the graph.
UNWIND $rows AS row
MERGE (f:Function {id: row.id})
SET f.name = row.name, f.file = row.file, f.start_line = row.start_line

UNWIND $edges AS e
MATCH (a:Function {id: e.src})
MATCH (b:Function {id: e.dst})
MERGE (a)-[r:CALLS]->(b)
SET r.line = e.line

Take quiz
Why create the uniqueness constraint before running MERGE at scale?
MERGE uses the backing index to find existing nodes, otherwise each lookup scans
Constraints compress the batches
MERGE refuses to run without one
What speeds up the very first load of a huge graph the most?
One transaction per node
neo4j-admin database import from CSV into an empty database
Running MERGE with no constraints

39. How do you resolve cross-language edges in a code knowledge graph?

Calls that cross a language or service boundary rarely appear as direct calls in an AST. They go through FFI (ctypes, JNI, cgo), HTTP or gRPC, or shared schemas such as OpenAPI and protobuf. The practical approach is to model the boundary as a node.

  1. Server side: read route decorators or annotations (@app.get("/users/{id}"), @GetMapping) and create (:Function)-[:HANDLES]->(:Endpoint {method, route}).
  2. Client side: extract URL literals or templates from HTTP-client calls and create (:Function)-[:CALLS_ENDPOINT]->(:Endpoint), matching on normalized method and route.
  3. For gRPC and protobuf, treat the .proto service definition as the authority and link generated stubs back to it.
  4. For FFI, match the declared native symbol name to the exported function in the C or C++ code.
MATCH (client:Function)-[:CALLS_ENDPOINT]->(e:Endpoint)<-[:HANDLES]-(server:Function)
RETURN client.qualified_name, e.route, server.qualified_name

Tag these edges as heuristic with a confidence value. URLs are often assembled at runtime and FFI targets are sometimes loaded by name, so matches can be missed or wrong.

Take quiz
How are an HTTP client call in one service and its handler in another best linked?
By matching function names across repositories
Through a shared Endpoint node matched by normalized method and route
By comparing file sizes
Why mark cross-language edges as heuristic?
A property graph cannot store them otherwise
Heuristic edges execute faster
URLs and FFI targets are often built dynamically, so matches can be wrong or missed

40. How do you version the graph across commits and branches?

There is no single right answer. These are the common designs:

Design How it works Trade-off
Snapshot per commit Full graph copy for each commit Simple, but storage explodes
Temporal properties valid_from and valid_to commit on nodes and edges Stores only changes, but needs commit ordering
Content-addressed sharing Nodes keyed by content hash, (:Snapshot)-[:INCLUDES]->(node) Git-like structure sharing, more complex queries
Live graph plus PR overlays One graph for the default branch, short-lived graphs for pull requests Cheap, but no deep history

Branches make temporal ranges awkward because commit history is a DAG, not a line. A valid_to on one branch means nothing on another. Either answer "as of commit X" by checking ancestry, or switch to snapshot sets.

A good default is a live graph for the main branch with overlays for open pull requests, and temporal properties only if you truly need history queries.

Take quiz
What does a valid_from and valid_to scheme store per change?
A full copy of the graph for each commit
Nothing, history is read from git on every query
Only changed entities, stamped with the commit range in which they exist
Why do branches complicate valid_from and valid_to ranges?
Commit history is a DAG, not one linear sequence
Branches cannot share files
Commit hashes are not unique

41. Which is better for code retrieval: graph traversal, vector search or a hybrid, and why?

A hybrid is usually best, because each method fails where the other succeeds.

Method Strong at Weak at
Vector search Fuzzy intent: "where do we retry failed payments?" Exact identifiers, structure, completeness
Keyword or BM25 Exact symbol and error-message matches Paraphrased intent
Graph traversal Callers, definitions, impact, dependency paths Needs a starting node, cannot interpret vague questions

In a hybrid pipeline, vector and keyword search choose seed nodes, the graph expands from them along CALLS, IMPORTS and EXTENDS edges, and a reranker trims the result to the token budget.

Rule of thumb: conceptual questions start with vectors, structural questions ("who calls this", "what breaks if I change it") start with the graph, and anything with an exact name starts with keywords.

Take quiz
Which question is best answered by graph traversal?
Which functions call validateToken?
Find code that sounds like exponential backoff
Summarize what this repository is for
In a hybrid pipeline, what usually supplies the seed nodes?
A full graph scan ordered by node ID
Vector or keyword search over node text
The LLM's training data

42. How do you troubleshoot missing call edges?

Follow the pipeline stage by stage and add a counter at each one.

  1. Ingested? Check the File node exists and the file was not filtered out as generated or vendored.
  2. Parsed cleanly? Look for ERROR nodes around the call site.
  3. Captured? Run your tree-sitter query on that snippet. Variants such as obj.method(), await foo(), new Foo() and decorator calls are commonly missed when the query handles only a plain identifier call.
  4. Resolved? Check import aliases, star imports and dynamic attribute access.
  5. Loaded? A pattern like MATCH (a), (b) MERGE (a)-[:CALLS]->(b) silently produces no rows if either MATCH finds nothing, so no error is raised. Compare emitted edge counts with stored counts.
  6. Target excluded? The callee may live in a vendored path with no stub.
MATCH (f:Function {qualified_name: $caller})
OPTIONAL MATCH (f)-[r:CALLS]->(g)
RETURN f.file, f.start_line, collect(g.qualified_name) AS callees

Take quiz
Why can a MATCH ... MERGE edge load silently lose edges?
MERGE deletes edges with unknown targets
If either MATCH finds no node, zero rows reach MERGE and no error is raised
Cypher rejects the whole batch with an error
What is the first check when a call edge is missing?
Increase the Neo4j heap size
Rebuild all vector embeddings
Confirm the file was ingested and parsed without errors

43. How do you design the graph for monorepos with millions of nodes?

The aim is to keep the graph queryable by keeping it deliberately small and well partitioned.

  1. Declaration-level granularity. Keep packages, files, classes and functions as nodes. Do not store every expression. Keep full ASTs as Parquet or object storage keyed by file hash and load them on demand.
  2. Partition by repository, package or team with a repo property or separate databases. Cross-partition edges point at stub nodes.
  3. Stub external dependencies rather than ingesting them.
  4. Index only lookup keys: id, qualified_name, path.
  5. Pre-aggregate. Derive module-level DEPENDS_ON edges from function-level calls so architecture questions do not traverse millions of edges.
  6. Tame supernodes. A logging helper with 50,000 callers makes traversals explode. Exclude nodes above a degree threshold from expansion or collapse their edges into counts.
  7. Index incrementally with sharded workers.

The common failure is modelling for completeness. A graph that stores everything answers nothing quickly.

Take quiz
How do you keep a monorepo graph small enough to query?
Store every expression as a node
Merge all files of a package into one node
Store declaration-level nodes and keep full ASTs outside the graph
What is the supernode problem in a code graph?
A node such as a logging helper with huge degree makes traversals explode
A node with no properties
A node that stores the whole repository

44. How do you prevent secrets and proprietary code leaking through the graph?

A code graph concentrates sensitive material and makes it easy to query, so treat it as sensitive data from day one.

  1. Scan before ingest with a secret detector (gitleaks or trufflehog style) and redact findings.
  2. Do not store string literal values by default. Keep kind and length, or a hash. Literals are where API keys and passwords live.
  3. Store minimal text. Signatures and docstrings are often enough. Bodies only where retrieval needs them.
  4. Authorize at the query layer. Carry repo or team on every node and enforce it in a fixed query wrapper, not in LLM-written Cypher. Alternatively use one database per tenant or role-based access control (Neo4j Enterprise).
  5. Apply the same ACLs to embeddings. They are derived from the code and can leak it.
  6. Log retrieved nodes per request so access can be audited.
  7. Treat retrieved text as untrusted. Comments in indexed code can contain instructions aimed at your agent (prompt injection).
Take quiz
Why not store string literal values by default?
They may contain API keys or passwords that would then be queryable
They slow parsing down
Strings are not part of the AST
Why apply the same access rules to embeddings?
Vector indexes encrypt them automatically
Embeddings are derived from the code and can leak information about it
They cannot be searched anyway

45. Explain how an LLM coding agent uses the graph to complete a change?

The graph is a map that tells the agent where to look. The source files remain the truth. A typical request, "add retry to charge()", flows like this:

sequenceDiagram
  participant U as User
  participant A as Agent
  participant G as Code Graph
  participant R as Repo
  U->>A: Add retry to charge()
  A->>G: Find symbol charge (name + vector)
  G-->>A: Definition, file, line range
  A->>G: Callers and tests of charge (CALLS backward)
  G-->>A: Affected functions and test files
  A->>R: Read exact lines, apply edit
  A->>R: Run affected tests
  A->>G: Re-index changed files
  G-->>A: Updated nodes and edges
  A-->>U: Diff and test results

The graph saves tokens (no reading whole directories), finds the tests worth running, and shows blast radius before editing. The agent still reads the real lines before changing them, because the graph is an index and may lag behind the working tree.

Take quiz
Why should the agent read the source file after querying the graph?
The graph stores only binary data
The graph is an index that may lag behind the working tree
Cypher cannot return more than one row
Which query finds the tests to run after changing charge()?
A forward CALLS traversal from test files to main()
All File nodes sorted by size
A backward CALLS traversal from charge() filtered to test files

46. How do you validate graph correctness against the compiler or LSP?

Treat an independent source of truth as an oracle and measure the graph against it.

  1. Golden fixtures. Small repositories per language with hand-labelled expected nodes and edges, asserted in CI.
  2. Oracle comparison. For sampled functions, compare CALLS and REFERENCES edges with LSP textDocument/references, a SCIP index, or compiler or type-checker output. Report precision and recall per edge type.
  3. Invariant queries. Every Function has exactly one DEFINED_IN file, no relationship has a missing endpoint, no duplicate IDs, start_line <= end_line, and per-file node counts match the parse.
  4. Differential test. Build from scratch and build incrementally to the same commit, then compare hashes of the sorted node and edge sets. This catches stale or missing data from incremental updates.
  5. Metrics. Watch the unresolved-call ratio per language over time.

Expect lower recall for dynamic languages. The goal is to know the number, not to hit 100 percent.

Take quiz
What does comparing incremental and from-scratch builds catch?
Syntax errors in the grammar
Slow network connections
Bugs where incremental updates leave stale or missing nodes and edges
What do you compute against an LSP oracle?
Precision and recall of edges per edge type
Average file length
Embedding cosine similarity

47. What is the difference between syntactic and semantic edges, and why does it matter?

Syntactic edges come directly from the shape of the tree. Semantic edges need name or type resolution.

Aspect Syntactic Semantic
Examples CONTAINS, HAS_PARAMETER, DEFINES CALLS, REFERENCES, EXTENDS, OVERRIDES, IMPORTS target
Needs One file's AST Cross-file context and resolution
Reliability Exact if the parse succeeded Can be incomplete or wrong
Invalidated by Changes to its own file Changes in other files too

It matters for three reasons. Consumers should be able to trust the two kinds differently, so tag edges with kind and confidence. The two have different rebuild costs, because syntactic extraction parallelizes per file while semantic resolution needs ordering. And incremental updates must treat them separately: a semantic edge can go stale even though its own file never changed.

Take quiz
Which edge is purely syntactic?
A class CONTAINS a method
A method OVERRIDES a parent method
A function CALLS a function in another file
Why can a semantic edge become stale when its own file is unchanged?
Semantic edges expire after 24 hours
It depends on declarations in other files that may change
Syntactic edges overwrite them

48. How does taint analysis use a code property graph?

Taint analysis asks whether untrusted data can reach a dangerous operation. In a CPG you mark sources (such as request.getParameter), sinks (the argument of executeQuery) and sanitizers (escaping functions). Data-flow edges link each definition to its uses, and the query looks for paths from a source to a sink that avoid every sanitizer.

MATCH p = (s:Source)-[:FLOWS_TO*1..15]->(k:Sink)
WHERE NONE(n IN nodes(p) WHERE n:Sanitizer)
RETURN s.file, s.start_line, k.file, k.start_line

Joern exposes the same idea through reachableByFlows over its CPG. Interprocedural flows need extra edges that connect arguments to parameters and return values to call results.

An AST-only graph cannot do this because it has no data-flow edges. Expect false positives when flows are over-approximated and false negatives around reflection and dynamic calls.

Take quiz
What does a taint query look for?
Any function that is never called
A data-flow path from an untrusted source to a sensitive sink with no sanitizer
Two functions with the same name
What does an AST-only graph lack for taint analysis?
Function names
File paths
Data-flow edges between definitions and uses

49. How do you handle macros, templates and code generation in the graph?

The AST of the source text is not always the code the compiler sees. Decide per case whether you model the source level or the expanded level, and record which one in an origin property.

Case Problem Approach
C/C++ preprocessor Tree-sitter sees unexpanded text, so macro-generated calls are invisible Use a compiler-backed AST (clang with compile_commands.json) when accuracy matters
C++ templates One template, many instantiations Node for the template, optional INSTANTIATES edges from compiler data
Rust macros Behavior hidden in macro expansion Use rust-analyzer or rustc expanded output, or accept unexpanded source
Python decorators A decorator can replace the function Add DECORATED_BY edges and treat framework decorators (routes, tasks) as meaningful
Generated code (protobuf, ORM) Huge, regenerated, not hand-written Ingest with generated: true and link GENERATED_FROM to the schema

The mistake to avoid is silently mixing both levels. A call that exists only after macro expansion should be marked as such, so nobody treats a missing edge as proof that nothing calls the function.

Take quiz
Why might a tree-sitter AST of a C file miss real calls?
Header files are always empty
C has no function calls
Calls produced by macro expansion do not appear in the unexpanded source text
Which edge links a Python function to a framework decorator such as @app.get?
DECORATED_BY
EXTENDS
OVERRIDES

50. How can you optimize graph queries for deep traversals?

Deep traversals on code graphs blow up quickly because call graphs are dense and cyclic. Apply these in order:

  1. Anchor the query on an indexed start node (id or qualified_name) instead of a label scan.
  2. Bound the depth (*1..4). Unbounded * is rarely what you want.
  3. Filter early by relationship type and direction, and by edge confidence using ALL(r IN relationships(p) WHERE r.confidence >= 0.8).
  4. Return DISTINCT nodes. Many paths reach the same node, and cycles multiply them.
  5. Materialize frequent answers, such as (:Module)-[:DEPENDS_ON]->(:Module) derived at ingestion, so architecture questions do not walk function-level paths.
  6. Use the Graph Data Science library for reachability, PageRank or community detection on an in-memory projection instead of Cypher paths.
  7. Run PROFILE and look for AllNodesScan and Cartesian products. Handle supernodes by degree.
PROFILE
MATCH (f:Function {id: $id})<-[:CALLS*1..4]-(c:Function)
RETURN DISTINCT c.id

Take quiz
Why return DISTINCT nodes from a variable-length query on a cyclic call graph?
Many different paths can reach the same node, producing duplicate rows
DISTINCT makes the traversal go deeper
Cycles make Cypher return no rows
What best speeds up a repeated 'module depends on module' question?
Raise the hop limit to unbounded
Materialize derived DEPENDS_ON edges at ingestion time
Drop the index on qualified_name
«
»

Comments & Discussions