Skip to content

Commit fc7c91d

Browse files
committed
feat(rust): update cache version to v114 and enhance AST extraction for test functions and array literals
1 parent 3a842e4 commit fc7c91d

4 files changed

Lines changed: 178 additions & 9 deletions

File tree

internal/cachecov/coverage_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ var versionCoverage = map[int][]string{
146146
112: {"TestStorage_TypeORMEntity", "TestStorage_DrizzleTable", "TestStorage_PrismaModels", "TestStorage_NoORMDependencyEmitsNothing", "TestIODirect_ORMCallSeedsPerformsIO", "TestGolden"}, // TypeScript emits storage facts for TypeORM @Entity / Drizzle pgTable / Prisma models (schema.prisma read off-glob); ORM query methods seed io_direct so performs_io propagates through a repository wrapper, making a per-iteration wrapper call a detectable N+1 (GAP-XL-04, new/26)
147147
113: {"TestJavaSPIServiceFileRefs", "TestJavaRuleNodeProp", "TestGolden"}, // Java folds SPI service-file registrations (META-INF/services + META-INF/dubbo) as KindFileRef references so an impl loaded by name is not a false orphan, and emits scanned_plugin on @RuleNode classpath-scanned plugins (GAP-JV-08, new/60)
148148
114: {"TestGolden", "TestOwnsFile", "TestAST_ImplTraitForType_EmitsImplements", "TestAST_CallInsideMacroArgument", "TestAST_MatchesMacroGuardCall", "TestAST_FunctionPassedAsCallbackArgument", "TestAST_OrdinaryArgument_NoPhantomReference", "TestAST_FunctionReferenceInStructField", "TestAST_FunctionReferenceTakenByAddress", "TestAST_SerdeDefaultAttribute_ReferencesFunction", "TestAST_SerdeSkipSerializingIfAttribute_ReferencesFunction", "TestAST_ClapValueParserAttribute_ReferencesFunction", "TestAST_LocalFunctionPassedAsCallbackWithinSameBody", "TestAST_FunctionReferenceNestedInsideMacroArgument", "TestAST_MergeStrategyAttribute_ReferencesScopedFunction", "TestExtract_UnprefixedUseOfSiblingFileSubmodule", "TestAST_ThiserrorAttributeCall_ReferencesFunction", "TestAST_MacroRulesBodyCall_EmitsFileRef", "TestAST_ItemLevelMacroInvocationCall_EmitsFileRef", "TestAST_DropImplTagsOverride", "TestAST_FutureImplTagsOverride", "TestAST_InherentDropLikeMethod_NotTaggedOverride", "TestAST_OtherTraitMethodNamedDrop_NotTaggedOverride", "TestAST_ScopedAssociatedFnCall_InstantiatesType", "TestAST_ScopedAssociatedFnCall_SelfNotInstantiated", "TestAST_BareScopedVariantValue_InstantiatesEnum", "TestAST_ScopedVariantInMatchPattern_InstantiatesEnum", "TestAST_ScopedMethodCall_NotTreatedAsVariant", "TestAST_BareUnitStructValue_Instantiates", "TestAST_BareUnitStructValue_LetBinding_Instantiates"}, // Rust extractor added: symbols, implements post-pass, dependency/calls/route facts, cache participation; macro-arg calls, function-as-value references, serde/clap/merge attribute refs, sibling-file submodule use-resolution, owner-less macro content via file_ref; Drop::drop/Future::poll tagged override; Type::assoc_fn()/Type::Variant/bare unit-struct value all record RelInstantiates for Type
149+
115: {"TestAST_TestFnAtFileRoot_NoSymbolFact", "TestAST_TestFnAtFileRoot_CreditsProductionCall", "TestAST_FunctionPointerArrayLiteral_ReferencesEach", "TestAST_SchemarsSchemaWithAttribute_ReferencesFunction", "TestAST_SchemarsSchemaWithQualifiedPath_ReferencesFunction", "TestAST_SelfCallAcrossSeparateImplBlocks_NotDropped", "TestAST_SelfCallWithinSameImplBlock_StillQualified"}, // Rust: #[test] fn recognized wherever it lives; array-literal dispatch tables (&[f, g]); schemars schema_with attribute (bare and crate::-qualified); self.method() no longer dropped across separate impl blocks/trait defaults
149150
}
150151

151152
func TestCacheVersionCoverage(t *testing.T) {

internal/engine/cache.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,18 @@ import (
456456
// handling. Rust is a new FileOwner, so its arrival also reshuffles which files count as
457457
// "shared" for every other cached extractor's key in a mixed-language repo. Cached
458458
// snapshots of any repo containing Rust files must re-extract.
459-
const cacheVersion = "v114"
459+
// v115: Rust recognizes a #[test]/#[tokio::test]/#[wasm_bindgen_test] fn wherever it
460+
// lives — a plain tests.rs file, an un-gated mod tests {} — not just inside a
461+
// #[cfg(test)] module; it gets no symbol fact, and its calls into production code are
462+
// credited via a file_ref/test_ref fact instead of counted as dead production code.
463+
// Also records a function referenced bare inside an array literal (a `&[f, g]`
464+
// dispatch table) as used, and resolves the schemars crate's
465+
// #[schemars(schema_with = "fn")] attribute string (bare or crate::-qualified)
466+
// like serde's default/skip_serializing_if. Fixes a latent bug where
467+
// self.method() was silently dropped (no edge at all) whenever method wasn't
468+
// a sibling of the immediately enclosing impl block — e.g. a type with
469+
// several impl blocks, or a trait's own default method.
470+
const cacheVersion = "v115"
460471

461472
// extractorCache holds per-extractor facts keyed by a content hash of the files
462473
// the extractor depends on. It is loaded from disk at the start of a snapshot and

internal/extractors/rustextractor/rust_ast.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,23 +231,48 @@ func (w *astWalker) isKnownSubmodule(name string) bool {
231231
// places a Rust test module can appear; impl/trait bodies never contain one.
232232
func (w *astWalker) walkItemsTrackingAttrs(parent *sitter.Node) {
233233
sawCfgTest := false
234+
sawTestAttr := false
234235
for i := uint(0); i < uint(parent.ChildCount()); i++ {
235236
c := parent.Child(i)
236237
if c.Kind() == "attribute_item" {
237-
if isCfgTestAttribute(nodeText(c, w.src)) {
238+
text := nodeText(c, w.src)
239+
if isCfgTestAttribute(text) {
238240
sawCfgTest = true
239241
}
242+
if isTestAttribute(text) {
243+
sawTestAttr = true
244+
}
240245
continue
241246
}
242-
if c.Kind() == "mod_item" && sawCfgTest {
247+
switch {
248+
case c.Kind() == "mod_item" && sawCfgTest:
243249
w.enterTestMod(c)
244-
} else {
250+
case c.Kind() == "function_item" && sawTestAttr:
251+
saved := w.inTestMod
252+
w.inTestMod = true
253+
w.walkTestItem(c)
254+
w.inTestMod = saved
255+
default:
245256
w.walkChild(c)
246257
}
247258
sawCfgTest = false
259+
sawTestAttr = false
248260
}
249261
}
250262

263+
// isTestAttribute reports whether an attribute_item marks its function as a
264+
// test (#[test], #[tokio::test], #[wasm_bindgen_test]) — catching a #[test]
265+
// fn wherever it lives (a plain tests.rs file, an un-gated mod tests {}),
266+
// not just inside a #[cfg(test)] module.
267+
func isTestAttribute(text string) bool {
268+
inner := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(text), "#["), "]")
269+
if i := strings.IndexAny(inner, "(["); i >= 0 {
270+
inner = inner[:i]
271+
}
272+
inner = strings.TrimSpace(inner)
273+
return inner == "test" || strings.HasSuffix(inner, "::test") || inner == "wasm_bindgen_test"
274+
}
275+
251276
// isCfgTestAttribute reports whether an attribute_item's raw text is a
252277
// `#[cfg(test)]`-shaped gate. A coarse substring check (rather than requiring
253278
// an exact `cfg(test)` match) also catches compound forms like
@@ -753,7 +778,7 @@ func (w *astWalker) walkForCalls(node *sitter.Node) {
753778
w.handleStructExpression(node)
754779
case "token_tree":
755780
w.scanTokenTreeCalls(node)
756-
case "arguments":
781+
case "arguments", "array_expression":
757782
w.scanArgumentReferences(node)
758783
case "field_initializer":
759784
w.emitValueReference(node.ChildByFieldName("value"))
@@ -822,8 +847,9 @@ func (w *astWalker) scanTokenTreeCalls(node *sitter.Node) {
822847
}
823848
}
824849

825-
// scanArgumentReferences checks each argument for a function passed by name
826-
// (`.map_err(f)`), which produces no call_expression since `f` isn't applied.
850+
// scanArgumentReferences checks each element (a call argument or array
851+
// literal item) for a function passed by name (`.map_err(f)`, `&[f, g]`
852+
// dispatch tables), which produces no call_expression since it isn't applied.
827853
func (w *astWalker) scanArgumentReferences(node *sitter.Node) {
828854
for i := uint(0); i < node.NamedChildCount(); i++ {
829855
w.emitValueReference(node.NamedChild(i))
@@ -891,7 +917,11 @@ func (w *astWalker) handleCallExpression(node *sitter.Node) {
891917
case calleeSelfRef:
892918
if methods := w.currentMethods(); methods[name] {
893919
w.emitEdge(facts.RelCalls, w.dir+"."+w.qualify(name))
920+
break
894921
}
922+
// Not a sibling of this impl block (another impl block, a trait
923+
// default) — still unambiguously a method call, so fall back.
924+
w.emitEdge(facts.RelCalls, name)
895925
case calleeOther:
896926
// Receiver/path type is unknown without full type inference. Emitting
897927
// the bare member name still lets short-name dead-code matching mark
@@ -951,11 +981,11 @@ func (w *astWalker) ensureFileRefFact() int {
951981
var attrFnRefKeys = map[string]bool{
952982
"default": true, "skip_serializing_if": true,
953983
"serialize_with": true, "deserialize_with": true, "with": true,
954-
"value_parser": true, "strategy": true,
984+
"value_parser": true, "strategy": true, "schema_with": true,
955985
}
956986

957987
// attrFnRefMacros: attribute macro names worth scanning for attrFnRefKeys.
958-
var attrFnRefMacros = map[string]bool{"serde": true, "arg": true, "merge": true}
988+
var attrFnRefMacros = map[string]bool{"serde": true, "arg": true, "merge": true, "schemars": true}
959989

960990
// scanAttributeFnRefs walks a struct/enum body for #[serde(...)]/#[arg(...)]
961991
// attributes referencing a function by name.
@@ -1003,6 +1033,9 @@ func (w *astWalker) scanAttribute(attr *sitter.Node) {
10031033
case "string_literal":
10041034
if content := findChildByKind(v, "string_content"); content != nil {
10051035
name = nodeText(content, w.src)
1036+
if idx := strings.LastIndex(name, "::"); idx >= 0 {
1037+
name = name[idx+2:]
1038+
}
10061039
}
10071040
case "identifier":
10081041
// A scoped path (mod::path::fn) is flattened into separate

internal/extractors/rustextractor/rust_ast_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,3 +1094,127 @@ fn make() {
10941094
t.Errorf("expected RelInstantiates -> SqlCommentSanitizer, got %+v", f.Relations)
10951095
}
10961096
}
1097+
1098+
func TestAST_TestFnAtFileRoot_NoSymbolFact(t *testing.T) {
1099+
ff := extractAST(t, `
1100+
use super::*;
1101+
1102+
#[test]
1103+
fn approvals_reviewer_serializes_auto_review() {
1104+
assert_eq!(1, 1);
1105+
}
1106+
`)
1107+
if _, ok := findFact(ff, "pkg.approvals_reviewer_serializes_auto_review"); ok {
1108+
t.Error("a #[test] fn at file scope (e.g. a plain tests.rs) should not become a production symbol fact")
1109+
}
1110+
}
1111+
1112+
func TestAST_TestFnAtFileRoot_CreditsProductionCall(t *testing.T) {
1113+
ff := extractAST(t, `
1114+
fn helper() {}
1115+
1116+
#[test]
1117+
fn calls_helper() {
1118+
helper();
1119+
}
1120+
`)
1121+
if _, ok := findFact(ff, "pkg.calls_helper"); ok {
1122+
t.Error("a #[test] fn should not become a symbol fact")
1123+
}
1124+
refs := findFactsByKind(ff, facts.KindTestRef)
1125+
if len(refs) != 1 || !hasRelation(refs[0], facts.RelCalls, "pkg.helper") {
1126+
t.Errorf("expected a KindTestRef -> pkg.helper, got %+v", refs)
1127+
}
1128+
}
1129+
1130+
func TestAST_FunctionPointerArrayLiteral_ReferencesEach(t *testing.T) {
1131+
ff := extractAST(t, `
1132+
type Pass = fn(&mut Value);
1133+
const PASSES: &[Pass] = &[strip_a, drop_b];
1134+
fn strip_a(v: &mut Value) {}
1135+
fn drop_b(v: &mut Value) {}
1136+
`)
1137+
c, ok := findFact(ff, "pkg.PASSES")
1138+
if !ok {
1139+
t.Fatal("expected fact for pkg.PASSES")
1140+
}
1141+
for _, want := range []string{"pkg.strip_a", "pkg.drop_b"} {
1142+
if !hasRelation(c, facts.RelCalls, want) {
1143+
t.Errorf("expected RelCalls -> %s, got %+v", want, c.Relations)
1144+
}
1145+
}
1146+
}
1147+
1148+
func TestAST_SchemarsSchemaWithAttribute_ReferencesFunction(t *testing.T) {
1149+
ff := extractAST(t, `
1150+
pub struct Event {
1151+
#[schemars(schema_with = "event_name_schema")]
1152+
name: String,
1153+
}
1154+
fn event_name_schema(g: &mut SchemaGenerator) -> Schema { todo!() }
1155+
`)
1156+
s, ok := findFact(ff, "pkg.Event")
1157+
if !ok {
1158+
t.Fatal("expected fact for pkg.Event")
1159+
}
1160+
if !hasRelation(s, facts.RelCalls, "event_name_schema") {
1161+
t.Errorf("expected RelCalls -> event_name_schema, got %+v", s.Relations)
1162+
}
1163+
}
1164+
1165+
func TestAST_SchemarsSchemaWithQualifiedPath_ReferencesFunction(t *testing.T) {
1166+
ff := extractAST(t, `
1167+
pub struct Features {
1168+
#[schemars(schema_with = "crate::schema::features_schema")]
1169+
flags: Vec<String>,
1170+
}
1171+
fn features_schema(g: &mut SchemaGenerator) -> Schema { todo!() }
1172+
`)
1173+
s, ok := findFact(ff, "pkg.Features")
1174+
if !ok {
1175+
t.Fatal("expected fact for pkg.Features")
1176+
}
1177+
if !hasRelation(s, facts.RelCalls, "features_schema") {
1178+
t.Errorf("expected RelCalls -> features_schema, got %+v", s.Relations)
1179+
}
1180+
}
1181+
1182+
func TestAST_SelfCallAcrossSeparateImplBlocks_NotDropped(t *testing.T) {
1183+
ff := extractAST(t, `
1184+
pub struct Foo;
1185+
impl Foo {
1186+
fn helper(&self) {}
1187+
}
1188+
impl Foo {
1189+
fn caller(&self) {
1190+
self.helper();
1191+
}
1192+
}
1193+
`)
1194+
f, ok := findFact(ff, "pkg.Foo.caller")
1195+
if !ok {
1196+
t.Fatal("expected fact for pkg.Foo.caller")
1197+
}
1198+
if !hasRelation(f, facts.RelCalls, "helper") {
1199+
t.Errorf("expected RelCalls -> helper, got %+v", f.Relations)
1200+
}
1201+
}
1202+
1203+
func TestAST_SelfCallWithinSameImplBlock_StillQualified(t *testing.T) {
1204+
ff := extractAST(t, `
1205+
pub struct Foo;
1206+
impl Foo {
1207+
fn helper(&self) {}
1208+
fn caller(&self) {
1209+
self.helper();
1210+
}
1211+
}
1212+
`)
1213+
f, ok := findFact(ff, "pkg.Foo.caller")
1214+
if !ok {
1215+
t.Fatal("expected fact for pkg.Foo.caller")
1216+
}
1217+
if !hasRelation(f, facts.RelCalls, "pkg.Foo.helper") {
1218+
t.Errorf("expected qualified RelCalls -> pkg.Foo.helper, got %+v", f.Relations)
1219+
}
1220+
}

0 commit comments

Comments
 (0)