
trailmark v0.5.0
Build and query a graph database representation of source code
Trailmark
Parse source code into queryable graphs of functions, classes, calls, and semantic annotations for security analysis.
Trailmark uses tree-sitter for language-agnostic AST parsing and rustworkx for high-performance graph traversal. The long-term vision is to combine this graph with mutation testing and coverage-guided fuzzing to identify gaps between assumptions and test coverage that are reachable from user input.
How It Works
Trailmark operates in three phases: parse, index, and query.
flowchart TD
A["Source Files"] --> B["tree-sitter Parser"]
B --> C["CodeGraph (nodes + edges)"]
C --> D["rustworkx GraphStore"]
D --> E["QueryEngine"]
E --> F["JSON / Summary / Hotspots"]
classDef src fill:#007bff26,stroke:#007bff,color:#007bff
classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
classDef query fill:#ffc10726,stroke:#e6a817,color:#e6a817
class A src
class B parse
class C,D data
class E,F query
1. Parse
A language-specific parser walks the directory, parses each file into a tree-sitter AST, and extracts:
- Nodes — functions, methods, classes, structs, interfaces, traits, enums, modules, namespaces
- Edges — calls, inheritance, implementation, containment, imports
- Metadata — type annotations, cyclomatic complexity, branches, docstrings, exception types
Supported Languages
| Language | Extensions | Key constructs |
|---|---|---|
| Python | .py | functions, classes, methods |
| JavaScript | .js, .jsx, .mjs, .cjs | functions, classes, arrow functions |
| TypeScript | .ts, .tsx | functions, classes, interfaces, enums |
| PHP | .php | functions, classes, interfaces, traits |
| Ruby | .rb | methods, classes, modules |
| C | .c, .h | functions, structs, enums |
| C++ | .cpp, .hpp, .cc, .hh, .cxx, .hxx | functions, classes, structs, namespaces |
| C# | .cs | methods, classes, interfaces, structs, enums, namespaces |
| Java | .java | methods, classes, interfaces, enums |
| Go | .go | functions, methods, structs, interfaces |
| Rust | .rs | functions, structs, traits, enums, impl blocks, verus! blocks |
| Solidity | .sol | contracts, interfaces, libraries, functions, modifiers, structs, enums |
| Cairo | .cairo | functions, traits, structs, enums, impl blocks, StarkNet contracts |
| Circom | .circom | templates, functions, signals, components |
| Haskell | .hs | functions, data types, type classes, instances |
| Erlang | .erl | functions, records, behaviours, modules |
| Miden Assembly | .masm | procedures, entrypoints, constants, invocations |
| Swift | .swift | functions, classes, structs, enums, protocols, extensions |
| Objective-C | .m, .mm, .h | C functions, classes, methods (selector-based naming) |
| Kotlin | .kt, .kts | functions, classes, interfaces, data classes, objects, methods |
| Dart | .dart | functions, classes, abstract classes, methods, constructors |
| Move | .move | modules, functions, imports, direct calls |
| Tact | .tact | contracts, structs, receivers, functions |
| Func | .fc, .func | functions, includes, direct calls |
| Sway | .sw | ABI interfaces, structs, impl methods, functions |
| Rego | .rego | packages, imports, policy rules, rule calls |
| Proto | .proto | services, RPCs, messages, fields, enums |
| Thrift | .thrift | services, functions, structs, fields, enums |
| GraphQL | .graphql, .gql | object types, root operations, fields, enums |
| SQL | .sql | schemas, tables, views, functions, procedures |
The Rust parser extracts ordinary, spec, and proof functions with bodies
from verus! { ... } blocks. The Verus grammar currently supports only
brace-delimited blocks; verus!(...) and verus![...] are skipped with a warning.
Inline modules retain qualified IDs (for example, file.inner:function), and
methods retain their impl or trait owner. Each block is parsed independently;
syntax errors produce a warning with the file and block location.
Qualified invocations such as vstd::prelude::verus! are supported too.
Local helper functions retain their enclosing function in their IDs (for example,
file:outer.inner). Calls to these helpers and relative imports such as
use super::* resolve to declarations present in the file.
Repeated local names in different blocks receive a byte-offset suffix to keep
their nodes distinct, and calls respect block scope.
Local structs, enums, traits, and inline modules retain the enclosing function
too: for example, file:outer.Guard.drop and file:outer.local.helper.
Their bodies have separate calls and complexity. Explicit calls to known local
types and modules resolve within the file. Bare calls search function and module
scopes; associated items use paths such as Type::method. Dotted calls resolve
only when the receiver names a known struct or enum. Other value receivers,
including self.method(), remain unresolved. Implicit destructor calls and receiver
type inference are not modeled. These rules apply to ordinary Rust as well as
Verus blocks. Editing code before a declaration with an offset suffix changes
its ID, so trailmark diff can report that declaration as removed and added.
Trailmark does not expand arbitrary macros. Literal Verus invocations inside
opaque macros or function bodies are skipped with a warning. Uninvoked
macro_rules! definitions are not expanded. Function signatures without bodies
and calls in requires/ensures clauses are not extracted.
flowchart TD
subgraph "Per-File Parsing"
F["Source file"] --> TS["tree-sitter AST"]
TS --> EX["Extract nodes"]
TS --> EC["Extract call edges"]
TS --> EB["Count branches"]
TS --> ET["Resolve types"]
end
EX --> CG["CodeGraph"]
EC --> CG
EB --> CG
ET --> CG
classDef src fill:#007bff26,stroke:#007bff,color:#007bff
classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
classDef extract fill:#ffc10726,stroke:#e6a817,color:#e6a817
classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
class F src
class TS parse
class EX,EC,EB,ET extract
class CG data
Node IDs follow the scheme module:function, module:Class, or module:Class.method for unambiguous lookup. Directory parsing resolves bare cross-file calls when a unique definition exists; ambiguous cross-file calls are left at their original best-effort target and marked uncertain. Edge confidence is tagged as certain (direct calls, self.method()), inferred (attribute access on non-self objects), or uncertain (dynamic dispatch or ambiguous resolution).
2. Index
The GraphStore loads the CodeGraph into a rustworkx PyDiGraph and builds bidirectional ID/index mappings for fast traversal.
3. Query
The QueryEngine provides a high-level API over the indexed graph: