Tree-sitter is a syntactic parser — it gives us the AST but no type information. When parsing repo.findById(id), tree-sitter produces:
method_invocation
object: identifier "repo" ← variable name, NOT the type
name: identifier "findById"
arguments: [identifier "id"]
To know that repo.findById is actually UserRepository::findById, we need to resolve the type of repo — a job normally done by the compiler, not a parser.
Record calls exactly as they appear in syntax. No type resolution.
@fn saveUser(User)->User @calls[repo.save, cache.invalidate, validateUser]
repo.save— variable name + method name, type unknownvalidateUser— same-class call, implicitlythis- Pro: trivial to implement, never wrong
- Con: AI must infer types from surrounding context
Use type information already available within the same file/class to resolve variable types.
Type information sources:
| Source | Example | Confidence |
|---|---|---|
| Field declarations | private UserRepository repo; |
High |
| Method parameters | fn process(parser: &Parser) |
High |
| Local variables with explicit type | val x: Foo = ... / Foo x = ... |
High |
| Constructor calls | let x = Parser::new() / var x = new Parser() |
High |
| Static / type-level calls | UserService.getInstance() |
100% |
this/self calls |
this.validate() / self.parse() |
100% |
| Return type chaining | getRepo().save() — requires knowing return type of getRepo() |
Medium |
Example resolution:
class UserService {
private UserRepository repo; // field type known
private CacheManager cache; // field type known
public User saveUser(User user) { // param type known
repo.save(user); // → UserRepository::save (from field)
cache.invalidate(user.getId()); // → CacheManager::invalidate (from field)
validateUser(user); // → this::validateUser (same class)
}
}Estimated coverage: ~85-90% of calls in a typical codebase.
Combine Level 1 with import/use statements to resolve fully qualified module paths.
import com.example.repository.UserRepository;
// → UserRepository references resolve to com.example.repository.UserRepositoryuse crate::parser::Parser;
// → Parser::new() resolves to crate::parser::Parser::newThis does not help resolve more calls, but enriches already-resolved calls with cross-module path information.
After resolving forward calls (Level 1 & 2), Skelecode performs a final graph pass to link targets back to their callers.
CallerRef: EachMethodorFunctionnow contains acallers: Vec<CallerRef>list.- Searchable Architecture: Enables the "Called by" view in TUI and the
called-by::back-links in Obsidian. - Bi-directional Topology: Allows the Renderer to draw arrows from target to source in Graph views.
Requires compiler-level analysis: tracking types through var/auto inference, generic instantiation, trait resolution, etc. This is effectively rebuilding half the compiler for each language.
Not planned. The cost-to-benefit ratio is poor for a structural analysis tool.
| Phase | Goal | Status |
|---|---|---|
| Phase 7 | Local Type Heuristics (this, self, fields) | ✅ Done |
| Phase 8 | Cross-module Import Resolution | ✅ Done |
| Phase 10 | Reverse Call Graph (Bi-directional linking) | ✅ Done |
| Future | Chaining & Type Alias follow-through | 🛠️ Backlog |
When a call cannot be resolved, the output keeps the raw syntactic form:
# Fully resolved
@fn saveUser(User)->User @calls[UserRepository::save, CacheManager::invalidate, this::validateUser]
# Partially resolved (repo type unknown)
@fn process()->void @calls[repo.doSomething, this::validate]
The raw variable name (repo.doSomething) signals to the reader that this call is unresolved. AI can still infer the type from field declarations elsewhere in the output.
These cases are not worth solving for a structural analysis tool:
| Case | Example | Why skip |
|---|---|---|
| Dynamic dispatch / polymorphism | animal.speak() — Dog or Cat? |
Requires runtime information |
| Reflection | method.invoke(obj, args) |
Cannot be resolved statically |
| Higher-order functions as calls | list.map(this::transform) |
Complex, low structural value |
| Deep method chaining | a.b().c().d() |
Requires full type inference |
| JS computed property access | obj[methodName]() |
Dynamic, cannot resolve |
| Closures / lambdas calling outer scope | { callback() } |
Ambiguous target |
These cases represent an estimated ~5-10% of calls in real-world codebases. The trade-off of skipping them (implementation complexity vs. marginal value) is acceptable.
- Field types always explicit → high resolution rate
- Watch for: anonymous classes, lambda expressions, method references (
Class::method) var(Java 10+) requires right-hand-side analysis
- JS: no type annotations on fields/params → resolution mostly limited to constructor patterns and
thiscalls - TS: explicit types available → resolution similar to Java
- Watch for: destructuring, spread operators, prototype-based patterns
- Explicit types on properties → good resolution
val/varwith inferred types → requires right-hand-side analysis- Extension functions:
receiver.extFn()— need to know receiver type - Watch for: scope functions (
let,apply,run) change whatthis/itrefers to
selfmethods → 100% resolvable withinimplblockType::method()static calls → 100% resolvable- Watch for: trait method calls (need to know which trait is in scope), closures,
?operator chaining let x: Type = ...explicit annotations → resolvablelet x = exprwithout annotation → needs inference (skip in v1)