Static analysis
Static analysis is learning what a program does by reading its structure rather than by running it, which is the only way to reason about code that is never executed.
Static analysis is learning what a program does by reading its structure rather than by running it. It is the only way to reason about code that is never executed, and it is the engine that turns a parsed tree of a file into a set of resolved relationships: which function calls which, which value flows where, what could break if a line changes.
Two ways to learn what code does
There are broadly two ways to find out what a piece of software does. You can run it and watch, or you can read it and reason. These are so different in what they can tell you that they have their own names.
Dynamic analysis is learning by running. You feed the program real inputs, let it execute, and observe what actually happens: which lines ran, what values the variables held, how long it took, what it printed. It is the difference between reading a recipe and actually cooking the dish. Dynamic analysis is grounded in reality, because it reports what truly occurred, not what might occur.
Static analysis is learning by reading, without running anything. You take the source code, parse it into its structured form, and reason about what it must do, or could do, from the structure alone. It is reading the recipe closely enough to say, before any pan is hot, that this step depends on the previous one, that this ingredient is never used, that if you double this line you had better double that one too.
Each sees what the other cannot. Dynamic analysis only ever tells you about the exact runs you performed. If a line did not run this time, dynamic analysis has nothing to say about it, and most real programs have paths that almost never run: the error handler for a disk that is full, the branch for a leap second, the code behind a feature flag that is off in every test. Static analysis, by contrast, reads every line whether it runs or not. That is its defining strength. It is the only way to reason about code that is never, or almost never, executed, which in a large system is a great deal of the code.
What you can learn without running anything
It surprises people how much can be recovered from reading alone, once the code is in its structured form. From the parsed tree of a program, and the scope rules that bind each name to its definition, static analysis can establish, among other things:
- Every function, class, type, and constant the code defines, and where each is introduced.
- Which function calls which, by resolving each call to the exact definition it names.
- Which files import which, and so which parts of the system depend on which others.
- Which class inherits from which, forming the family tree of types.
- Whether a declared name is ever used at all, or is dead weight left behind.
- Which values a piece of data could hold, or which paths a computation could take.
Notice the shape of these answers. Some are certainties, this function is defined here, this file imports that one, because they are written plainly in the structure. Others are possibilities, this value could be one of these three things, because the exact one depends on inputs that only exist at run time. Static analysis lives comfortably in both registers, and knowing which kind of answer you are holding is part of using it well.
Control flow and data flow
Two ideas do most of the work in static analysis, and both have plain intuitions behind them.
Control flow is the set of paths execution could take through the code: the order in which things could happen. A program is not read strictly top to bottom, because an if statement forks the path in two, a loop bends the path back on itself, and a function call jumps to another place and returns. Control flow is the map of all these possible routes. Think of the streets of a city. Control flow is not the trip you took today; it is the full network of roads and the turns you are allowed to make, every route that is possible whether or not anyone drives it.
Data flow is the tracking of values as they move along those paths: where a value is created, where it travels, and where it is finally used. If a user types their name into a form, data flow follows that name from the input, through the functions that pass it along, to the place it is written into the database or shown on a screen. Where control flow is the roads, data flow is following one particular package as it is carried from address to address across them.
These two together are the heart of most of what static analysis produces. Ask what could break if I change this function and you are really asking a control flow question, which routes lead here, whose answer is the callers and the callers of those callers. Ask where does this password end up and you are asking a data flow question. The relationships that fill a code knowledge graph are, at bottom, control flow and data flow made explicit and written down.
Control flow is every route the roads allow. Data flow is following one package as it travels those routes. Most questions about code are one of these two in disguise.
Soundness and precision: the central tradeoff
Static analysis cannot always be both complete and exact at once, and the tension between those two goals is the most important thing to understand about it. The two goals have names.
An analysis is sound if it never misses a real case. If it claims to list all the callers of a function, a sound analysis includes every genuine caller, with no true one left out. An analysis is precise if it does not include cases that are not really there. A precise list of callers contains only genuine callers, with no false ones added. In an ideal world an analysis would be perfectly sound and perfectly precise. In the real world, for most interesting questions, you cannot have both, because the exact truth depends on run time facts that reading alone cannot pin down.
So an analysis leans one way or the other, and the two directions have plain meanings.
- Over-approximation favors soundness. When unsure, it includes the case. It would rather warn you about a caller that turns out not to be real than let a real caller slip past. The result may contain some false positives, extra entries that are not truly there, but it will not miss a genuine one.
- Under-approximation favors precision. When unsure, it leaves the case out. It would rather stay quiet than raise something it cannot confirm. The result contains only cases it is confident about, but it may miss some real ones.
Which to prefer depends on the cost of each kind of mistake. For a question like what could break if I change this, a missed case is dangerous, because it is the silent kind of failure: you change the code, nothing warns you, and the thing you did not know about fails later. Here you want over-approximation. A few extra things to check is a small price; a missed dependency is not. For a question where you will act automatically on every result, a false entry may be the worse cost, and you lean the other way. The honest position is that the tradeoff is real and cannot be wished away, and a good tool tells you which way it leans so you can read its answers correctly.
The limits, stated plainly
Static analysis is powerful, and it also has hard limits that come from the same source: it reads the code but does not run it, so anything the code decides only at run time is, in the general case, beyond certain reach. Three limits matter most in practice.
Dynamic dispatch. In object oriented code, a call like shape.area() does not name a single function. Which area runs depends on what kind of shape the value is at that moment, a circle, a square, a triangle, and that kind may not be decided until the program runs. Reading alone, the analysis often cannot say which one of several possible methods a call will land on. The sound answer is all of them that it could be, which is correct but less precise than a single arrow.
Reflection and dynamic loading. Some languages let a program build the name of a function or class as a string while it runs and then call it by that string, or load code that did not exist when the file was read. To a reader there is no call written down at all, only a string and a general mechanism. No amount of careful reading can always recover which concrete thing will be invoked, because the choice is genuinely not present in the text.
Config-driven behavior. A great deal of modern software decides what to do based on values that live outside the code: a configuration file, an environment variable, a row in a database, a flag set by an operator. The code says do whatever the config selects. Reading the code tells you the menu of possibilities but not the choice, because the choice is not in the code. This is one reason a code graph benefits from signals beyond the syntax alone, such as configuration files that wire services together and the history of which files tend to change in the same commit.
These limits are not flaws to be embarrassed about. They are the natural boundary of any technique that reasons from structure. The mature response is to name them, to lean toward soundness where a miss is costly, and to combine static reading with other evidence where reading alone runs out. Koragraph MCP, for instance, resolves calls, imports, and inheritance from the parsed structure, and also draws on cross-service signals and on git history, mined into a local database, so that connections which the syntax cannot show, like two files that keep changing together, are still recorded.
The engine that builds the map
Step back and the role of static analysis in the larger picture becomes clear. The parse gives you a tree, a faithful structure for a single file, but a tree on its own is still just the shape of one file’s text. It does not yet know that the call written on line ten reaches the function defined in a different file, or that changing one class ripples out to twenty others. Turning that inert structure into a web of resolved relationships is exactly what static analysis does. It carries the scope rules through the program, binds every name to its definition, follows control flow and data flow across files, and writes down the edges it finds.
That is why it is fair to call static analysis the engine of the whole enterprise. Declaration extraction supplies the nouns. Symbols and scope decide what each name refers to. Static analysis is the process that puts those together and produces the verbs, the calls and imports and inheritance and flows that connect one thing to another. The finished product of that work is the map itself.
Where this sits in the larger picture
Static analysis rests on the abstract syntax tree, which gives it a reliable structure to read instead of raw text, and on symbols, scope and binding, which let it resolve each name to the one thing it means rather than to a lookalike. With those in hand, it produces the relationships that the most practical questions depend on.
Two of those questions have their own concepts. When you ask what a change could affect, and want every caller and dependent that a change might touch, you are asking for a blast radius, and the honest answer leans toward soundness for exactly the reasons above. When you ask who invokes whom across the whole program, the assembled answer is the call graph. Both are static analysis put to work: the engine turns a tree into resolved relationships, and those relationships are what make a code knowledge graph worth walking.
Connected concepts
Where this sits
