Koragraph

Type hierarchies and inheritance

Inheritance edges record which types are built from which others, which is what lets a graph answer a question asked of a base type about all the code that specialises it.

A type hierarchy is the family tree of a program’s types: a record of which kinds of things are built out of which other kinds. Inheritance edges write that tree down as facts, one parent and child at a time, and those facts are what let a graph take a question you ask about a general type and answer it for every specific type that was built from it.

Start with what a type is

Before hierarchy, the flat idea. A type is a name for a shape of data together with the things you are allowed to do with it. A whole number is a type. A piece of text is a type. So is something bigger and custom, like an Account that holds a balance and an owner and knows how to deposit and withdraw. The type says what fields the thing carries and what operations it supports. Once a type exists, you can make many individual values that follow its shape. Each of those is an instance. The type is the mold; the instances are the things pressed from it.

Most languages give you a way to define your own types. Depending on the language they are called classes, structs, records, or interfaces, and the words carry small differences, but the core idea is shared: you name a shape, you list its data, and you attach behavior to it. A method is just a function that belongs to a type and can see that type’s own data. When you call the deposit method on a particular account, the method runs against that account’s balance and no other.

Why one type is built from another

Now the reason hierarchy exists at all. Real programs are full of types that are almost the same. A savings account and a checking account share nearly everything: both have a balance, both have an owner, both can take a deposit. They differ in a few details. A savings account earns interest; a checking account allows an overdraft. If you wrote each one from scratch, you would copy the shared parts into both, and then every fix to the shared parts would have to be made twice, and one day it would be made once and forgotten in the other.

Inheritance is the tool that removes the copy. You write the shared parts once, in a general type, and then you say that the specific types are built from it and add only what is different. The general type is called the base type, or the parent, or the superclass. The specific type built from it is the derived type, or the child, or the subclass. The derived type automatically has everything the base type has, plus whatever it adds or changes.

The everyday version of this is how we describe animals. A dog is a mammal. You do not have to restate that a dog is warm blooded and has a spine, because those facts come with being a mammal, and a mammal is an animal, so a dog is an animal too. The specific inherits from the general. Say “mammal” and you have said something true about every dog, cat, and whale at once. That is exactly the leverage inheritance gives a program, and it is exactly the leverage a graph can use later.

Interfaces: a promise without the parts

There is a second, lighter form of the same idea. Sometimes you do not want to share the actual code, only the promise that certain operations exist. An interface is a list of operations with no bodies: a contract that says any type claiming to satisfy me must provide these methods. A type that provides them is said to implement the interface.

Think of a wall socket. The socket is an interface. It promises a certain shape and a certain voltage. A lamp, a charger, and a kettle all implement that interface by having a matching plug. The socket knows nothing about lamps or kettles. It only knows that whatever plugs in has agreed to the contract. In code, an interface lets you write something that works with any type that keeps the promise, without knowing or caring which specific type showed up. Implementing an interface is another parent-to-child relationship, so it belongs in the same family tree as inheritance even though no shared code changes hands.

Polymorphism: one name, many behaviors

Here is where the hierarchy earns its keep, and where it starts to bite. Suppose the base type Account has a method called applyMonthlyUpdate. The savings account overrides it to add interest. The checking account overrides it to charge a fee if the balance is negative. Now you write a single loop that walks a list of accounts and calls applyMonthlyUpdate on each one. You never say which kind of account you are holding. You just call the method by its name on the base type.

At runtime, each account runs its own version. The savings accounts add interest; the checking accounts charge fees; and if a third kind of account is added next year, the same loop handles it too, with no change to the loop. This is polymorphism, which means “many shapes.” One call site, many possible behaviors, chosen by the actual type of the thing in hand. The mechanism that picks the right version at the moment of the call is called dynamic dispatch. Dynamic means the choice happens while the program is running, not while it is being written, because only at run time is it known which specific account you are actually holding.

A call written against a base type is not one destination. It is a question whose answer is chosen at run time from every type that specializes the base.

Why this makes call graphs hard

A call graph is the map of which piece of code calls which other piece. For a plain function it is direct: this call goes to that function, one arrow, done. Polymorphism breaks that cleanness. When code calls applyMonthlyUpdate through the base type, the honest answer to “where does this call go” is not one place. It is every override of applyMonthlyUpdate in every type that descends from Account. The loop that never mentions savings or checking can still, at run time, land inside either one.

This is why a tool that only reads the text of a program cannot draw the true call graph. The text at the call site says applyMonthlyUpdate on an Account, and nothing more. It does not say which override runs, because the code was written precisely so that it would not have to say. To draw the real arrows, you have to know the type hierarchy: you have to know that SavingsAccount and CheckingAccount both descend from Account and both override that method, so a call on the base could reach either body.

Consider the concrete stakes. You are about to change how applyMonthlyUpdate works on the base Account. You want to know everything that could be affected. Search the text for applyMonthlyUpdate and you find the base and the two overrides. But you also need every place that calls it, and many of those calls are written against Account with no visible link to the specific types at all. Without the hierarchy, you would miss that a change to the base contract ripples into behavior that only the subclasses provide. The connection is real. It is just not spelled out at the point where it matters.

Inheritance edges as facts

The way to make this tractable is to stop treating the hierarchy as something to rediscover on every question and instead record it once, as data. An inheritance edge is a single stored fact: this type is derived from that type, or this type implements that interface. Recover every such fact across a codebase and you have the whole family tree written down, ready to be queried rather than reconstructed.

With those edges in hand, questions that were hard become lookups. Given a base type, list every type derived from it, directly or several steps down. Given a method on a base type, find every override of it, because each override lives on a type you can reach by walking the inheritance edges downward. Given a call written against the base, enumerate the bodies it could actually reach. The graph does not guess. It follows edges it already resolved.

Koragraph is one system that resolves and stores these edges. It parses repositories with tree-sitter, a tool that reads source code and recovers its exact structure, and among the relationships it resolves are inheritance edges, alongside calls and imports. Those facts go into a local database that persists across sessions, so the hierarchy does not have to be rebuilt from scratch every time a question is asked. It covers twelve languages, including the ones where inheritance and interfaces are everyday tools: Java, C#, C++, Python, Ruby, Swift, TypeScript, and the rest.

A small worked example

Picture three types and one shared method.

Account            defines applyMonthlyUpdate
  SavingsAccount   overrides applyMonthlyUpdate (adds interest)
  CheckingAccount  overrides applyMonthlyUpdate (charges fee)

As plain text, the call in your monthly job reads only as a call to applyMonthlyUpdate on an Account. As inheritance edges, the same situation reads as three facts: SavingsAccount is derived from Account, CheckingAccount is derived from Account, and both carry their own applyMonthlyUpdate. Ask “what runs when the monthly job fires” and the graph walks from the base method down the two edges to the two overrides and answers with both bodies. Ask “if I change the base method, what family of types is affected” and it answers with the whole subtree. The difference is not a clever search. It is having recorded the parent-and-child relationship as a fact you can traverse.

Notice what changed between those two readings. The plain-text view and the edge view describe the same three types and the same call. What the edges add is not information the text lacked on the page; it is information the text refused to make explicit at the call site, because the whole design of polymorphism is to let one call stand in for many. Recording the inheritance edges is how you get that hidden structure back without having to re-derive it by hand each time you need it.

Where it gets fuzzy, honestly

Type hierarchies are not a full escape from uncertainty. A method call on a base type could in principle reach any override, but at a given moment it reaches exactly one, and static facts alone cannot always tell you which. If a program creates types by name from a string, or loads them from a plugin discovered at run time, the set of possible subclasses may not even be fully knowable before the program runs. Some languages allow a type to inherit from more than one parent, which turns the tree into a mesh and makes “which version wins” a rule you have to know rather than read off the shape. The graph gives you the honest set of possibilities. Narrowing that set to the one real destination on a particular run is a separate problem, and sometimes it can only be answered by watching the program actually execute.

None of that undercuts the value of recording the edges. The whole point is to move from “I have no idea which bodies a base call can reach” to “here is the exact, bounded set.” A bounded set of real possibilities is a far better starting point than a text search that cannot even see the relationship.

Where this sits in the bigger picture

Inheritance edges are one kind of relationship among several that together make a code knowledge graph, the explicit map of how a codebase is connected rather than what its files happen to say. Inheritance edges are the part of that map that captures family resemblance between types, and they are what let the call graph stay honest about polymorphism instead of pretending every call has a single obvious destination.

So this idea leans on two neighbors. It feeds directly into the call graph, which is the map of what calls what and which cannot be drawn correctly without knowing the type hierarchy behind polymorphic calls. And it depends on static analysis, the practice of learning true facts about a program by reading its structure rather than running it, which is how inheritance edges get recovered from source in the first place. Read those two next, and the reason type hierarchies matter as more than a tidy way to avoid copied code will come into full focus.

Connected concepts

The code knowledge graphA code knowledge graph is a structured map of a codebase where every declaration is a node and every real relationship, calls, imports, inheritance, cross-service links, is an edge.The call graphA call graph records which functions call which, so that following the edges forward gives everything a function depends on and backward gives everything that depends on it.Static analysisStatic 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.

Where this sits

Back to the full graphThe short glossary