Skip to content
23 changes: 22 additions & 1 deletion docs/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ on interface nodes stores the expected method set for implementation matching.
| TypeScript | Full | Full | Full | Full + Meta["methods"] | Full | Full | Full |
| JavaScript | Full | Full | Full | - | Full | Full | Full |
| Python | Full | Full | Full | - | Full | Full | Partial |
| Rust | Full | Full (impl blocks) | Full | Full + Meta["methods"] | Full | Full | Full |
| Rust | Full (+ `extern` blocks) | Full (impl blocks) | Structs/Enums/Unions/Aliases | Full + Meta["methods"] | Full (+ `extern crate`) | Full (+ macro bodies) | Consts/Statics |
| Java | Full | Full | Full | Full + Meta["methods"] | Full | Full | Fields |
| C# | Full | Full | Full | Full + Meta["methods"] | Full | Full | Fields |
| Kotlin | Full | Full | Full | Full | Full | Full | Properties |
Expand All @@ -85,6 +85,27 @@ on interface nodes stores the expected method set for implementation matching.
| OCaml | Full | Full (class) | Types/Modules | Module types | open | Full | Full |
| Lua | Full | Full (M.func/M:method) | - | - | require() | Full | Full |

### Rust specifics

`mod` is a namespace rather than a dependency, so it indexes as a package node
(the C++/C# namespace precedent) and items inside it carry `Meta["scope_mod"]`
while keeping flat ids. Two same-named types in one file both survive via the
shared id-disambiguation helper.

Rust does not parse macro arguments as expressions — every `macro_invocation`
holds an opaque token tree — so calls written inside one are recovered by a
token scan and labelled `Meta["via_macro"]`; filter on it to keep only
grammar-certain call edges. Pattern and cfg-predicate macros (`matches!`,
`cfg!`) are excluded, since their bodies hold patterns rather than calls. This
matters most for tests, where the call a `#[test]` exercises normally sits
inside an `assert*!`.

Entry points are stamped for `fn main` in a Cargo binary target, async-runtime
mains (`#[tokio::main]` and friends), test/bench harness attributes, and FFI
exports (`#[no_mangle]`, `#[wasm_bindgen]`, pyo3, napi). Declarations inside an
`extern` block are stamped `rust:ffi-import` — their body lives in another
language, so a missing definition is not a missing implementation.

Recent extraction refinements (each covered by a per-feature CI golden): Java `@interface` annotation types index as interfaces and participate in implementation matching; Java `new T(){…}` and C# `new { … }` anonymous classes/types become synthetic type nodes with an `extends` edge (to the instantiated type, or to `object` for C#); JS/TS arrow-valued class fields (`x = () => {…}`) are emitted as callable methods; JS/TS named imports emit one `imports` edge per binding (alias-aware via `Edge.Alias`) and barrel re-exports emit `re_exports` edges; JS/TS cross-file imports resolve onto the target file/symbol for relative specifiers (`./x`, `../x`) and for `tsconfig.json` / `jsconfig.json` `compilerOptions.paths` / `baseUrl` path aliases (`@/lib/x`), so callers / usages / blast-radius span aliased imports the same as relative ones; chained / factory-call receivers (`New().Build()`) carry an inferred `receiver_type`. See [features.md](features.md) and [architecture.md](architecture.md) for the edge semantics.

## Data, config, build
Expand Down
2 changes: 2 additions & 0 deletions internal/entrypoints/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func Detect(relPath, lang string, nodes []*graph.Node, edges []*graph.Edge) int
return detectASPNet(slashed, nodes) + detectDotNetFramework(nodes, edges)
case "java":
return detectJava(nodes, edges)
case "rust":
return detectRust(slashed, nodes, edges)
}
return 0
}
Expand Down
122 changes: 122 additions & 0 deletions internal/entrypoints/entrypoints_rust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package entrypoints

import (
"strings"

"github.com/zzet/gortex/internal/graph"
)

const rustAnnoPrefix = "annotation::rust::"

// rustEntryFnAnnos maps a function attribute to the entry-point kind it
// implies. Each names a symbol some runtime other than application code
// calls: an async runtime's generated main, a test harness, a benchmark
// harness, or a foreign runtime reaching in across an FFI boundary.
var rustEntryFnAnnos = map[string]string{
"tokio::main": "rust:async-main",
"actix_web::main": "rust:async-main",
"actix_rt::main": "rust:async-main",
"async_std::main": "rust:async-main",
"rocket::main": "rust:async-main",
"test": "rust:test",
"tokio::test": "rust:test",
"actix_rt::test": "rust:test",
"async_std::test": "rust:test",
"bench": "rust:bench",
"no_mangle": "rust:ffi-export",
"wasm_bindgen": "rust:ffi-export",
"pyfunction": "rust:ffi-export",
"pymethods": "rust:ffi-export",
"napi": "rust:ffi-export",
"unsafe(no_mangle)": "rust:ffi-export",
}

// rustEntryTypeAnnos maps a type attribute to its entry-point kind. A
// clap derive makes the type the process's argument surface, and a pyo3
// module or class is instantiated by the Python interpreter.
var rustEntryTypeAnnos = map[string]string{
"pyclass": "rust:ffi-export",
"pymodule": "rust:ffi-export",
"wasm_bindgen": "rust:ffi-export",
"napi": "rust:ffi-export",
}

// detectRust flags the symbols a Rust runtime reaches without any
// in-crate caller. Without it `fn main`, every #[test], and every
// #[no_mangle] FFI export look unreachable to a call-graph walk, so a
// binary crate's entire call tree reads as dead code.
func detectRust(relPath string, nodes []*graph.Node, edges []*graph.Edge) int {
annos := map[string]map[string]bool{}
for _, e := range edges {
if e.Kind != graph.EdgeAnnotated {
continue
}
name, ok := strings.CutPrefix(e.To, rustAnnoPrefix)
if !ok {
continue
}
if annos[e.From] == nil {
annos[e.From] = map[string]bool{}
}
annos[e.From][name] = true
}

count := 0
for _, n := range nodes {
switch {
case isFnOrMethod(n.Kind):
kind := ""
for a := range annos[n.ID] {
if k, ok := rustEntryFnAnnos[a]; ok {
kind = k
break
}
}
// Cargo runs `fn main` in a binary or example target; the
// crate itself never calls it.
if kind == "" && n.Name == "main" && rustBinaryTarget(relPath) {
kind = "rust:main"
}
// An extern "C" declaration is the foreign half of an FFI
// boundary — its body lives in another language, so a
// missing definition is not a missing implementation.
if kind == "" && n.Meta != nil && n.Meta["external"] == true &&
n.Meta["is_extern"] == true {
kind = "rust:ffi-import"
}
if kind != "" && stamp(n, kind) {
count++
}
case n.Kind == graph.KindType || n.Kind == graph.KindInterface:
for a := range annos[n.ID] {
if k, ok := rustEntryTypeAnnos[a]; ok {
if stamp(n, k) {
count++
}
break
}
}
}
}
return count
}

// rustBinaryTarget reports whether a path is one Cargo builds as an
// executable. Cargo's layout convention is what decides this: src/main.rs
// and src/bin/*.rs are binaries, and examples/ and benches/ are run
// directly too. A `fn main` anywhere else is an ordinary function.
func rustBinaryTarget(relPath string) bool {
switch {
case relPath == "main.rs" || strings.HasSuffix(relPath, "/main.rs"):
return true
case strings.Contains(relPath, "src/bin/"):
return true
case strings.HasPrefix(relPath, "examples/") || strings.Contains(relPath, "/examples/"):
return true
case strings.HasPrefix(relPath, "benches/") || strings.Contains(relPath, "/benches/"):
return true
case strings.HasPrefix(relPath, "tests/") || strings.Contains(relPath, "/tests/"):
return true
}
return false
}
89 changes: 89 additions & 0 deletions internal/entrypoints/entrypoints_rust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package entrypoints

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser/languages"
)

// detectRustFor runs the real Rust extractor over src and then the
// detector, so the test exercises the annotation shape the extractor
// actually emits rather than a hand-built one.
func detectRustFor(t *testing.T, relPath, src string) map[string]*graph.Node {
t.Helper()
e := languages.NewRustExtractor()
result, err := e.Extract(relPath, []byte(src))
require.NoError(t, err)
Detect(relPath, "rust", result.Nodes, result.Edges)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
return byID
}

// Without a Rust arm, `fn main` was never a live root and a binary
// crate's entire call tree read as dead code.
func TestDetectRust_MainInBinaryTarget(t *testing.T) {
byID := detectRustFor(t, "src/main.rs", `fn main() { run(); }
fn run() {}
`)
m := byID["src/main.rs::main"]
require.NotNil(t, m)
assert.Equal(t, true, m.Meta[MetaEntryPoint])
assert.Equal(t, "rust:main", m.Meta[MetaEntryKind])

assert.NotEqual(t, true, byID["src/main.rs::run"].Meta[MetaEntryPoint],
"an ordinary function is not an entry point")
}

// A `fn main` in a library module is an ordinary function — only
// Cargo's binary targets are executables.
func TestDetectRust_MainOutsideBinaryTargetIsNotAnEntryPoint(t *testing.T) {
byID := detectRustFor(t, "src/helpers.rs", `fn main() {}`)
assert.NotEqual(t, true, byID["src/helpers.rs::main"].Meta[MetaEntryPoint])
}

func TestDetectRust_AsyncMainAndTests(t *testing.T) {
byID := detectRustFor(t, "src/lib.rs", `#[tokio::main]
async fn main() {}

#[test]
fn it_works() {}

#[tokio::test]
async fn async_works() {}
`)
assert.Equal(t, "rust:async-main", byID["src/lib.rs::main"].Meta[MetaEntryKind],
"#[tokio::main] marks an entry point even outside a binary target")
assert.Equal(t, "rust:test", byID["src/lib.rs::it_works"].Meta[MetaEntryKind])
assert.Equal(t, "rust:test", byID["src/lib.rs::async_works"].Meta[MetaEntryKind])
}

// An FFI export is called from another language entirely, so nothing in
// the crate refers to it.
func TestDetectRust_FFIExports(t *testing.T) {
byID := detectRustFor(t, "src/lib.rs", `#[no_mangle]
pub extern "C" fn gx_open() -> i32 { 0 }

#[wasm_bindgen]
pub fn greet() {}
`)
assert.Equal(t, "rust:ffi-export", byID["src/lib.rs::gx_open"].Meta[MetaEntryKind])
assert.Equal(t, "rust:ffi-export", byID["src/lib.rs::greet"].Meta[MetaEntryKind])
}

// An extern block declares the foreign half of the boundary: its body
// lives in C, so a missing definition is not a missing implementation.
func TestDetectRust_ExternBlockDeclarationsAreNotDeadCode(t *testing.T) {
byID := detectRustFor(t, "src/ffi.rs", `extern "C" {
fn sqlite3_open(name: *const u8) -> i32;
}
`)
n := byID["src/ffi.rs::sqlite3_open"]
require.NotNil(t, n)
assert.Equal(t, "rust:ffi-import", n.Meta[MetaEntryKind])
}
Loading
Loading