Skip to content

Latest commit

 

History

History
159 lines (111 loc) · 6.25 KB

File metadata and controls

159 lines (111 loc) · 6.25 KB

Call Resolution Guide

The Challenge

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.

Resolution Levels

Level 0 — Raw Syntactic (baseline)

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 unknown
  • validateUser — same-class call, implicitly this
  • Pro: trivial to implement, never wrong
  • Con: AI must infer types from surrounding context

Level 1 — Heuristic Type Resolution (✅ Implemented)

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.

Level 2 — Import-Aware Resolution (✅ Implemented)

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.UserRepository
use crate::parser::Parser;
// → Parser::new() resolves to crate::parser::Parser::new

This does not help resolve more calls, but enriches already-resolved calls with cross-module path information.

Level 3 — Reverse Call Graph (✅ Implemented)

After resolving forward calls (Level 1 & 2), Skelecode performs a final graph pass to link targets back to their callers.

  • CallerRef: Each Method or Function now contains a callers: 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.

Level 4 — Full Type Inference (out of scope)

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.

Implementation Phases

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

Unresolved Call Representation

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.

Explicitly Out of Scope

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.

Language-Specific Notes

Java

  • 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

JavaScript / TypeScript

  • JS: no type annotations on fields/params → resolution mostly limited to constructor patterns and this calls
  • TS: explicit types available → resolution similar to Java
  • Watch for: destructuring, spread operators, prototype-based patterns

Kotlin

  • Explicit types on properties → good resolution
  • val/var with inferred types → requires right-hand-side analysis
  • Extension functions: receiver.extFn() — need to know receiver type
  • Watch for: scope functions (let, apply, run) change what this/it refers to

Rust

  • self methods → 100% resolvable within impl block
  • Type::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 → resolvable
  • let x = expr without annotation → needs inference (skip in v1)