Type-aware code search, all the way up the chain
Contents
You know that “Find Usages” feature that you’ve used a million times in your IDE? It works because the IDE compiled the project and holds the entire type graph in memory. That’s why it knows that something like myList.add(x) is a call to java.util.Collection.add even though the word “Collection” shows up nowhere on the line. Developers have leaned on that for years without thinking about it much, and it works great until the question outgrows the one project that’s open.
A coding agent has no such luxury as “Find Usages”. There’s no compiled project, no type graph, and nothing to right-click. Its default tools are text search and a file reader, so it’s forced to infer and re-derive from scratch every time what the compiler already knows. Working this way means it can easily miss certain call sites, but it prefers the ease and speed of searching text with grep over having to compile code and query type graphs.
That’s the gap Trigrep was built to close, and when we launched it, speed was the headline. That speed comes from the trigram index underneath, which rapidly narrows the scope using intersecting posting lists instead of scanning raw files. It holds up across thousands of repositories, or across one monolith far too large for an agent to grep, in sub-second time, so an agent can reach for it mid-task without burning latency or tokens waiting on an answer.
| Tool type | Technique | Strengths | Limitations |
|---|---|---|---|
| Grep family (grep, ripgrep, ag) | Raw text scan | Simple, local, immediate | Re-scans files each time; more reads required to confirm matches; very slow for large repos |
| Indexed code search tools | Trigram indexing | Fast discovery across many repos | Often limited integration with automation workflows |
| Semantic search (Moderne recipes) | Search LSTs via recipes | Precise understanding and transformation | Slower scan due to deeper analysis |
| Moderne Trigrep | Trigram indexing with LST data and org scoping | Fast, contextual, automation-ready discovery | Designed for fast discovery, not full semantic analysis |
Full type attribution, carried in the index
Speed isn’t the whole story, though. Because the index comes from the LST, it can carry the types, which means the agent stops having to trade accuracy for speed. Now, Trigrep carries full type attribution in the index itself, including the library types your code inherits from and calls into.
So it knows that myList.add(x) is a call to Collection.add, and it knows it across the whole portfolio in less time than grep would take. That pairing (compiler-grade type resolution at indexed-search speed and portfolio scale) is the part neither your IDE nor your grep can do, and it’s what makes searching reliable for agents to lean on for answering complex questions about code.
This all works because for every class, method, field, and reference in a file it indexes, Trigrep stores a symbol record in the index, with the related types already resolved.
That last part is the key piece. When a parser meets a library type at a call site, it often records just enough to identify it and leaves out that type’s inheritance hierarchy, what it extends and implements. A List shows up with no interfaces, and nothing in that record says it extends Collection. A text index, or even a naive symbol index, takes that at face value and misses roughly half the references you care about. Instead, Trigrep upgrades each type to its fully attributed form through the type-table chain. That chain is the authoritative ancestry for every type the repository touches, third-party libraries included.
The type chain makes search subclass-aware
Take the myList.add(x) call from the previous example, where myList is declared as a List. That call is really List.add, so a search for Collection.add won’t match it, even though List extends Collection. A symbol index that records only what’s literally written has the same blind spot.
Trigrep handles this when it builds the index instead of when you run the query. As it indexes a call made on a List, it walks up List’s ancestry and records that same call under every ancestor that declares its own add, including Collection.add. So sym:java.util.Collection.add matches the call site directly with no type reasoning at query time.
The effect is that Trigrep answers inheritance-shaped questions correctly across the whole hierarchy, including the parts that live in libraries you depend on. That ancestry lives in the type tables, so Trigrep doesn’t need the libraries themselves to know what extends what, and neither does the agent asking.
The new filter surface
Because the type chain is in the index, query filters can understand types. They take the same shape as the file:, repo:, and lang: clauses agents already know from other code search tools, so the grammar is familiar, only with new things to ask for.
A few of the filters that type attribution unlocks:
extends:andimplements:match by ancestor, computed through the chain.- Example:
implements:Repositoryfinds every class that implements it, including the ones that pick it up through a parent.
- Example:
returns:andthrows:match methods by return type and declared exceptions.- Example:
returns:Listfinds every method declared to return aList.
- Example:
annotated:matches declarations carrying a given annotation.- Example:
annotated:RestControllerfinds every declaration carrying that annotation, across every repository at once.
- Example:
sym:matches a symbol by fully qualified name, andcall:matches call sites by declaring type and name. Both are subclass-aware.- Example:
sym:java.util.Collection.addfinds themyList.add(x)call sites too, not only the ones written literally.
- Example:
visibility:,static:,final:, andabstract:filter by access and modifier, andselect:narrows to a declaration kind.- Example:
select:symbol.method static:yesfinds static method declarations only.
- Example:
Then there’s ref:. A bare ref:T finds the places where T explicitly appears as a type: a field or parameter declared as T, a new T(), a Javadoc reference. That’s close to what OpenRewrite’s FindTypes recipe returns. It skips expressions that merely happen to be of type T, which is what keeps the results usable. Search ref:org.slf4j.Logger and you get the declarations, rather than the thousands of log.info(...) calls that would bury them. When you do want those, ref.call:, ref.field:, and ref.owner: narrow to a specific kind, and ref.any: returns all of them. The default is the search you almost always meant.
The examples so far have all been Java, but none of this is Java-only. Because the filters read LST metadata rather than raw text, the same type-graph queries run against C#, JavaScript, and Python, so implements:System.IDisposable finds disposables in C# the same way implements:java.util.Comparator finds comparators in Java. Trigrep sees the whole type graph that the language’s LST carries.
You can experiment with these filters using the Trigrep search feature of the Code Genome Project. (More on that later.)

Questions no other search can answer
The filters matter because of the questions they collapse into a single search. A few that used to mean writing a recipe or reading through a lot of files by hand:
Every implementor of an interface, including the ones three (or ten) libraries deep rather than only the direct subclasses.
Every caller of a method reached through a subtype, so a deprecation sweep doesn’t miss
list.add(...)when the method it’s really calling isCollection.add.Every endpoint carrying a given framework annotation, across thousands of repositories at once.
Every method that returns a particular type, which is how you scope a migration to a reactive or nullable API before you touch a line.
Where a type is genuinely used as a type, separated from every expression that merely happens to be of that type, which is the difference between a useful
Listsearch and one buried in noise.
Each of these is a single Trigrep query, returning in the sub-second range across thousands of repositories.
Why this works now: LST v3 underneath
Full type attribution was always in the LST. What changed is that it’s now fully available in the index, and that came from a change one layer down in how the platform stores the LST. LST v3 replaces the monolithic serialization of earlier versions with separated tables, and two of those changes are what the current Trigrep is built on.
The first is per-dependency type tables. Instead of one type blob per repository, v3 writes one table per dependency version, keyed to its coordinate. In a Java project, the JDK gets its own, spring-boot:3.4.1 gets its own, and the source set’s own types get theirs. At read time they compose into a single flat type universe, which is exactly the chain Trigrep walks to resolve a receiver’s ancestry. Because a dependency version is identical everywhere it appears, its table is written once and reused across every repo that depends on it, so the intelligence behind a Collection.add query is shared instead of something that needs to be recomputed per repo.
That structure also keeps the cost down at query time. A symbol’s related types are stored as slots into those tables rather than as repeated names, so resolving one is a direct lookup rather than a scan. And a text-only or regex Trigrep query never needs this information, so it never opens a type table at all. Type awareness is only referenced on the searches that ask for it.
The second change is incremental builds. Each source file is a self-contained tree file, and each source set writes its own tables and its own Trigrep shard. Re-parsing one changed file rewrites that file and its shard, and re-assembly re-stitches only the chunks that changed. Keeping the index current across thousands of repositories becomes a byproduct of ordinary builds rather than a reindex-the-world job. For an agent working against a codebase that shifts through the day, the index reflects the code as it is now.
One more thing: it doesn’t stop at your code
Everything above assumes the type chain comes from your repositories and their dependencies. The same machinery works on code you never explicitly checked out. Trigrep can query LSTs built for public artifacts. LSTs can be built and indexed from third-party open source code by pulling its sources and POM, resolving its dependencies, and stitching the type tables together. So the same type-aware search runs over your open source dependencies the same way it runs over your portfolio. The engine doesn’t care whether the source started in your monorepo or in Maven Central.
That’s the idea behind the Code Genome Project: sequencing the public open source ecosystem into compiler-accurate LSTs so the code the world runs on can be searched by type, symbol, and call site rather than string match. The same Trigrep engine powers the search, and a remote MCP server opens the corpus to coding agents. It’s the type chain pointed at code that everyone uses.
Search became a type problem
Trigrep is still fast, and that still matters. But what changes how you and your agents use it is the type intelligence behind the speed. Trigrep understands the whole chain of types, including the libraries underneath your code, and it puts that understanding in the index where a query reaches it in the same instant it reaches a piece of text.
That’s what a coding agent needs from search: a way to ask a type-dependent question and get a type-accurate answer, cheap enough to ask a hundred times a session, and precise enough that the answer is the same set of files a recipe or your IDE would have found. A faster grep was only ever the starting point.
To try the type-aware filters against your own codebase, the Trigrep documentation has the full filter reference and query syntax, or try Trigrep with the Code Genome Project.


