Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

570 lines
14 KiB
Go

package languages
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// Regression: a bodyless trait-method signature must mint a
// <file>::<Trait>.<name> KindMethod node (marked trait_decl) — the node
// rust-analyzer binds every dispatch call site to. Parity with PHP's
// extractMethod / C#'s bodyless interface methods.
func TestRsExtractor_BodylessTraitMethodNode(t *testing.T) {
src := []byte(`trait Sink {
fn write(&self, msg: &str);
fn error_message(&self) -> String;
}
struct FileSink;
impl Sink for FileSink {
fn write(&self, msg: &str) {}
fn error_message(&self) -> String { String::new() }
}
`)
e := NewRustExtractor()
result, err := e.Extract("sink.rs", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
m := byID["sink.rs::Sink.write"]
require.NotNil(t, m, "bodyless trait method must mint a node")
assert.Equal(t, graph.KindMethod, m.Kind)
assert.Equal(t, "Sink", m.Meta["receiver"])
assert.Equal(t, "true", m.Meta["trait_decl"])
require.NotNil(t, byID["sink.rs::Sink.error_message"],
"a bodyless trait method with a return type mints a node too")
}
func TestRsExtractor_Function(t *testing.T) {
src := []byte(`fn greet(name: &str) -> String {
format!("Hello {}", name)
}
`)
e := NewRustExtractor()
result, err := e.Extract("main.rs", src)
require.NoError(t, err)
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
require.Len(t, funcs, 1)
assert.Equal(t, "greet", funcs[0].Name)
}
func TestRsExtractor_Struct(t *testing.T) {
src := []byte(`struct Config {
port: u16,
host: String,
}
`)
e := NewRustExtractor()
result, err := e.Extract("config.rs", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.Len(t, types, 1)
assert.Equal(t, "Config", types[0].Name)
}
func TestRsExtractor_EnumVariants(t *testing.T) {
src := []byte(`enum Shape {
Circle,
Rectangle(f64, f64),
Labeled { name: String },
}
`)
e := NewRustExtractor()
result, err := e.Extract("shape.rs", src)
require.NoError(t, err)
// Enum marked with Meta["kind"]="enum".
types := nodesOfKind(result.Nodes, graph.KindType)
require.Len(t, types, 1)
assert.Equal(t, "Shape", types[0].Name)
assert.Equal(t, "enum", types[0].Meta["kind"])
variants := map[string]bool{}
for _, n := range result.Nodes {
if n.Kind == graph.KindVariable && n.Meta != nil && n.Meta["kind"] == "enum_variant" {
variants[n.Name] = true
}
}
assert.Equal(t, map[string]bool{"Circle": true, "Rectangle": true, "Labeled": true}, variants)
}
func TestRsExtractor_StructFields(t *testing.T) {
src := []byte(`pub struct User {
pub id: u64,
pub name: String,
email: String,
}
`)
e := NewRustExtractor()
result, err := e.Extract("user.rs", src)
require.NoError(t, err)
fields := map[string]bool{}
for _, n := range result.Nodes {
if n.Kind == graph.KindField {
fields[n.Name] = true
}
}
assert.Equal(t, map[string]bool{"id": true, "name": true, "email": true}, fields)
}
func TestRsExtractor_Trait(t *testing.T) {
src := []byte(`trait Repository {
fn find_by_id(&self, id: &str) -> Option<User>;
}
`)
e := NewRustExtractor()
result, err := e.Extract("store.rs", src)
require.NoError(t, err)
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
require.Len(t, ifaces, 1)
assert.Equal(t, "Repository", ifaces[0].Name)
}
func TestRsExtractor_ImplMethods(t *testing.T) {
src := []byte(`struct Server {
port: u16,
}
impl Server {
fn new(port: u16) -> Self {
Server { port }
}
fn start(&self) {
println!("Starting on {}", self.port);
}
}
`)
e := NewRustExtractor()
result, err := e.Extract("server.rs", src)
require.NoError(t, err)
methods := nodesOfKind(result.Nodes, graph.KindMethod)
assert.Len(t, methods, 2) // new, start
// memberEdges now include the struct field (`port`) alongside the
// two impl methods — 3 total, all pointing at Server.
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
assert.Len(t, memberEdges, 3)
for _, e := range memberEdges {
assert.Equal(t, "server.rs::Server", e.To)
}
// Methods should NOT appear as top-level functions.
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
assert.Len(t, funcs, 0)
}
func TestRsExtractor_ImplMethodMeta(t *testing.T) {
src := []byte(`struct Foo {}
impl Foo {
fn bar(&self) {}
}
`)
e := NewRustExtractor()
result, err := e.Extract("foo.rs", src)
require.NoError(t, err)
methods := nodesOfKind(result.Nodes, graph.KindMethod)
require.Len(t, methods, 1)
assert.Equal(t, "bar", methods[0].Name)
assert.Equal(t, "foo.rs::Foo.bar", methods[0].ID)
assert.Equal(t, "Foo", methods[0].Meta["receiver"])
}
func TestRsExtractor_TraitMethods(t *testing.T) {
src := []byte(`trait Repository {
fn find_by_id(&self, id: &str) -> Option<User>;
fn save(&mut self, user: User);
}
`)
e := NewRustExtractor()
result, err := e.Extract("store.rs", src)
require.NoError(t, err)
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
require.Len(t, ifaces, 1)
methods, ok := ifaces[0].Meta["methods"].([]string)
require.True(t, ok)
assert.Equal(t, []string{"find_by_id", "save"}, methods)
}
func TestRsExtractor_Use(t *testing.T) {
src := []byte(`use std::collections::HashMap;
use tokio::net::TcpListener;
`)
e := NewRustExtractor()
result, err := e.Extract("main.rs", src)
require.NoError(t, err)
imports := edgesOfKind(result.Edges, graph.EdgeImports)
require.Len(t, imports, 2)
}
// TestRsExtractor_PubUseTaggedAsReExport checks that a `pub use`
// (and `pub(crate) use`) declaration tags its import edge as a
// re-export, while a plain private `use` does not — the signal the
// re-export chain follower needs to distinguish a transparent forward
// from a private import.
func TestRsExtractor_PubUseTaggedAsReExport(t *testing.T) {
src := []byte(`use crate::internal::Private;
pub use crate::api::Public;
pub(crate) use crate::api::Shared;
`)
e := NewRustExtractor()
result, err := e.Extract("lib.rs", src)
require.NoError(t, err)
reexport := map[string]bool{}
for _, edge := range edgesOfKind(result.Edges, graph.EdgeImports) {
isRe := edge.Meta != nil && edge.Meta["reexport"] == true
reexport[edge.To] = isRe
}
require.False(t, reexport["unresolved::import::crate/internal/Private"],
"a private `use` must not be tagged as a re-export")
require.True(t, reexport["unresolved::import::crate/api/Public"],
"`pub use` must be tagged as a re-export")
require.True(t, reexport["unresolved::import::crate/api/Shared"],
"`pub(crate) use` must be tagged as a re-export")
}
func TestRsExtractor_TypeEnv_ExplicitType(t *testing.T) {
src := []byte(`struct Config {
port: u16,
}
impl Config {
fn start(&self) {}
}
fn main() {
let cfg: Config = Config { port: 8080 };
cfg.start();
}
`)
e := NewRustExtractor()
result, err := e.Extract("main.rs", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var startCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "start") {
startCall = c
break
}
}
require.NotNil(t, startCall, "expected a call edge to start")
require.NotNil(t, startCall.Meta, "expected Meta on start call edge")
assert.Equal(t, "Config", startCall.Meta["receiver_type"])
}
func TestRsExtractor_TypeEnv_NewCall(t *testing.T) {
src := []byte(`struct Server {
port: u16,
}
impl Server {
fn new(port: u16) -> Self {
Server { port }
}
fn listen(&self) {}
}
fn main() {
let srv = Server::new(8080);
srv.listen();
}
`)
e := NewRustExtractor()
result, err := e.Extract("main.rs", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var listenCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "listen") {
listenCall = c
break
}
}
require.NotNil(t, listenCall)
require.NotNil(t, listenCall.Meta)
assert.Equal(t, "Server", listenCall.Meta["receiver_type"])
}
func TestRsExtractor_TypeEnv_Unknown(t *testing.T) {
src := []byte(`fn get_service() -> Box<dyn std::any::Any> {
todo!()
}
fn main() {
let svc = get_service();
svc.process();
}
`)
e := NewRustExtractor()
result, err := e.Extract("main.rs", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var processCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "process") {
processCall = c
break
}
}
require.NotNil(t, processCall)
assert.NotContains(t, processCall.Meta, "receiver_type", "unknown type should not produce a receiver_type hint")
}
func TestRsExtractor_TypeEnv_Chain(t *testing.T) {
src := []byte(`struct Order {
id: i32,
}
struct UserService {
name: String,
}
impl UserService {
fn get_order(&self) -> Order {
Order { id: 1 }
}
}
impl Order {
fn total(&self) -> i32 {
42
}
}
fn main() {
let svc: UserService = UserService { name: String::new() };
svc.get_order().total();
}
`)
e := NewRustExtractor()
result, err := e.Extract("app.rs", src)
require.NoError(t, err)
// Verify return_type is set on get_order method.
var getOrderNode *graph.Node
for _, n := range result.Nodes {
if n.Name == "get_order" {
getOrderNode = n
break
}
}
require.NotNil(t, getOrderNode, "expected a node for get_order")
assert.Equal(t, "Order", getOrderNode.Meta["return_type"])
// Verify chain resolution: svc.get_order() should resolve to Order.
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var totalCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "total") {
totalCall = c
break
}
}
require.NotNil(t, totalCall, "expected a call edge to total")
require.NotNil(t, totalCall.Meta, "expected Meta on total call edge")
assert.Equal(t, "Order", totalCall.Meta["receiver_type"])
}
func TestRsExtractor_DocAndVisibility(t *testing.T) {
src := []byte(`/// The greeter.
///
/// Used everywhere.
pub fn greet() {}
/// Internal helper.
fn helper() {}
/// Crate-only.
pub(crate) fn restricted() {}
/// The Server type.
pub struct Server {}
/// Trait Foo.
pub trait Foo {}
`)
e := NewRustExtractor()
result, err := e.Extract("lib.rs", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
greet := byID["lib.rs::greet"]
require.NotNil(t, greet)
if greet.Meta["visibility"] != "public" {
t.Fatalf("greet.vis = %q", greet.Meta["visibility"])
}
if greet.Meta["doc"] != "The greeter." {
t.Fatalf("greet.doc = %q", greet.Meta["doc"])
}
helper := byID["lib.rs::helper"]
require.NotNil(t, helper)
if helper.Meta["visibility"] != "private" {
t.Fatalf("helper.vis = %q", helper.Meta["visibility"])
}
restricted := byID["lib.rs::restricted"]
require.NotNil(t, restricted)
if restricted.Meta["visibility"] != "internal" {
t.Fatalf("restricted.vis = %q", restricted.Meta["visibility"])
}
server := byID["lib.rs::Server"]
require.NotNil(t, server)
if server.Meta["visibility"] != "public" {
t.Fatalf("Server.vis = %q", server.Meta["visibility"])
}
if server.Meta["doc"] != "The Server type." {
t.Fatalf("Server.doc = %q", server.Meta["doc"])
}
foo := byID["lib.rs::Foo"]
require.NotNil(t, foo)
if foo.Meta["visibility"] != "public" {
t.Fatalf("Foo.vis = %q", foo.Meta["visibility"])
}
if foo.Meta["doc"] != "Trait Foo." {
t.Fatalf("Foo.doc = %q", foo.Meta["doc"])
}
}
func TestRsExtractor_AnnotationEdges(t *testing.T) {
src := []byte(`#[derive(Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct User {
name: String,
}
#[test]
fn test_user() {}
#[deprecated(note = "use NewThing instead")]
pub fn old_thing() {}
`)
e := NewRustExtractor()
result, err := e.Extract("user.rs", src)
require.NoError(t, err)
annNames := map[string]bool{}
for _, n := range result.Nodes {
if v, _ := n.Meta["kind"].(string); v == "annotation" {
annNames[n.Name] = true
}
}
for _, want := range []string{"Debug", "Clone", "serde", "test", "deprecated"} {
if !annNames[want] {
t.Fatalf("missing annotation node %q (got %v)", want, annNames)
}
}
edges := map[string][]string{}
for _, e := range result.Edges {
if e.Kind != graph.EdgeAnnotated {
continue
}
edges[e.From] = append(edges[e.From], e.To)
}
userID := "user.rs::User"
if !rustTestContains(edges[userID], "annotation::rust::Debug") {
t.Fatalf("missing Debug derive on User, got %v", edges[userID])
}
if !rustTestContains(edges[userID], "annotation::rust::Clone") {
t.Fatalf("missing Clone derive on User, got %v", edges[userID])
}
if !rustTestContains(edges[userID], "annotation::rust::serde") {
t.Fatalf("missing serde on User, got %v", edges[userID])
}
testFnID := "user.rs::test_user"
if !rustTestContains(edges[testFnID], "annotation::rust::test") {
t.Fatalf("missing #[test] on test_user, got %v", edges[testFnID])
}
}
func rustTestContains(haystack []string, needle string) bool {
for _, h := range haystack {
if h == needle {
return true
}
}
return false
}
func TestRsExtractor_ThrowsResult(t *testing.T) {
src := []byte(`pub fn parse(s: &str) -> Result<i32, ParseError> { Ok(0) }
pub fn open() -> Result<File, std::io::Error> { unimplemented!() }
pub fn boxed() -> Result<u8, Box<dyn std::error::Error>> { Ok(0) }
pub fn no_error() -> i32 { 0 }
`)
e := NewRustExtractor()
result, err := e.Extract("lib.rs", src)
require.NoError(t, err)
throws := edgesOfKind(result.Edges, graph.EdgeThrows)
got := map[string]string{}
for _, e := range throws {
got[e.From] = e.To
}
assert.Equal(t, "unresolved::ParseError", got["lib.rs::parse"])
assert.Equal(t, "unresolved::Error", got["lib.rs::open"], "trailing ident of std::io::Error")
assert.Equal(t, "unresolved::Error", got["lib.rs::boxed"], "Box<dyn Error> should land on Error")
if _, ok := got["lib.rs::no_error"]; ok {
t.Fatalf("no_error has no Result return; should not throw")
}
}
func TestRsExtractor_GenericTypeParams(t *testing.T) {
src := []byte(`pub fn collect<T: Clone, U>(items: Vec<T>, mapper: impl Fn(T) -> U) -> Vec<U> {
items.into_iter().map(mapper).collect()
}
`)
e := NewRustExtractor()
result, err := e.Extract("lib.rs", src)
require.NoError(t, err)
var collect *graph.Node
for _, n := range result.Nodes {
if n.Name == "collect" && n.Kind == graph.KindFunction {
collect = n
}
}
require.NotNil(t, collect)
tp, _ := collect.Meta["type_params"].([]map[string]string)
require.Len(t, tp, 2)
assert.Equal(t, "T", tp[0]["name"])
assert.Equal(t, "Clone", tp[0]["bound"])
assert.Equal(t, "U", tp[1]["name"])
}