Skip to content

Commit 836b701

Browse files
tae2089claude
andcommitted
Only classify test-named functions as test nodes
mapDefTypeToNodeKind marked any declaration whose name started with the language test prefix as a test node, so a production type TestConfig or a function testimonialCard (prefix "test" + lowercase continuation) was miscategorized, which then excluded it from dead-code analysis and changed its search kind. Restrict the test check to function/method declarations and require a word boundary after a bare-word prefix (separator-terminated prefixes like "test_" already encode the boundary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b05b3af commit 836b701

2 files changed

Lines changed: 83 additions & 7 deletions

File tree

internal/parse/treesitter/walker.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -656,24 +656,48 @@ func (w *Walker) inferEnclosingReceiver(defNode *sitter.Node, content []byte) st
656656

657657
// mapDefTypeToNodeKind maps query definition labels to internal node kinds.
658658
// @intent keep language query captures aligned with graph node categorization
659-
// @domainRule declarations matching the configured test prefix become test nodes
659+
// @domainRule only function/method declarations whose name is a test name become test nodes;
660+
// types, classes, and interfaces are never tests even when their name starts with the prefix
660661
func (w *Walker) mapDefTypeToNodeKind(defType string, name string) model.NodeKind {
661-
if w.spec.TestPrefix != "" && strings.HasPrefix(name, w.spec.TestPrefix) {
662-
return model.NodeKindTest
663-
}
664-
665662
switch defType {
666663
case "class":
667664
return model.NodeKindClass
668665
case "interface", "type":
669666
return model.NodeKindType
670-
case "function", "method":
671-
return model.NodeKindFunction
672667
default:
668+
if isTestName(name, w.spec.TestPrefix) {
669+
return model.NodeKindTest
670+
}
673671
return model.NodeKindFunction
674672
}
675673
}
676674

675+
// isTestName reports whether name denotes a test given the language's test prefix.
676+
// @intent avoid misclassifying production symbols like testimonialCard or TestConfig as tests.
677+
// @domainRule a separator-terminated prefix (e.g. "test_") is a boundary on its own; a bare-word
678+
// prefix (e.g. "Test", "test", "TEST") must be followed by end-of-name or a non-lowercase character
679+
// so it does not swallow a longer lowercase word.
680+
func isTestName(name, prefix string) bool {
681+
if prefix == "" || !strings.HasPrefix(name, prefix) {
682+
return false
683+
}
684+
last := prefix[len(prefix)-1]
685+
if !isASCIILetterOrDigit(last) {
686+
return true
687+
}
688+
rest := name[len(prefix):]
689+
if rest == "" {
690+
return true
691+
}
692+
c := rest[0]
693+
return c < 'a' || c > 'z'
694+
}
695+
696+
// @intent classify ASCII identifier characters for test-prefix boundary detection.
697+
func isASCIILetterOrDigit(c byte) bool {
698+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
699+
}
700+
677701
// buildQualifiedName joins package, receiver, and declaration name into a stable identifier.
678702
// @intent generate graph keys that distinguish methods from package-level declarations
679703
func (w *Walker) buildQualifiedName(pkg, receiver, name string) string {

internal/parse/treesitter/walker_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,58 @@ func TestAdd(t *testing.T) {}
16811681
}
16821682
}
16831683

1684+
func TestParseGo_TestPrefixDoesNotMisclassifyProductionDecls(t *testing.T) {
1685+
src := `package main
1686+
1687+
type TestConfig struct{}
1688+
1689+
func Testimony() {}
1690+
1691+
func TestAdd(t *testing.T) {}
1692+
`
1693+
w := NewWalker(GoSpec)
1694+
nodes, _, err := w.Parse("main_test.go", []byte(src))
1695+
if err != nil {
1696+
t.Fatalf("unexpected error: %v", err)
1697+
}
1698+
testNodes := filterByKind(nodes, model.NodeKindTest)
1699+
if len(testNodes) != 1 || testNodes[0].Name != "TestAdd" {
1700+
t.Fatalf("expected only TestAdd as a test node, got %+v", testNodes)
1701+
}
1702+
// TestConfig (a type) and Testimony (Test + lowercase 'i') must not be tests.
1703+
byName := map[string]model.NodeKind{}
1704+
for _, n := range nodes {
1705+
byName[n.Name] = n.Kind
1706+
}
1707+
if byName["TestConfig"] == model.NodeKindTest {
1708+
t.Errorf("TestConfig type must not be a test node")
1709+
}
1710+
if byName["Testimony"] == model.NodeKindTest {
1711+
t.Errorf("Testimony (lowercase continuation) must not be a test node")
1712+
}
1713+
}
1714+
1715+
func TestParseTypeScript_TestPrefixRequiresWordBoundary(t *testing.T) {
1716+
src := `function testimonialCard() {}
1717+
function testLogin() {}
1718+
`
1719+
w := NewWalker(TypeScriptSpec)
1720+
nodes, _, err := w.Parse("app.ts", []byte(src))
1721+
if err != nil {
1722+
t.Fatalf("unexpected error: %v", err)
1723+
}
1724+
kinds := map[string]model.NodeKind{}
1725+
for _, n := range nodes {
1726+
kinds[n.Name] = n.Kind
1727+
}
1728+
if kinds["testimonialCard"] == model.NodeKindTest {
1729+
t.Errorf("testimonialCard must not be classified as a test")
1730+
}
1731+
if kinds["testLogin"] != model.NodeKindTest {
1732+
t.Errorf("testLogin should be classified as a test, got %v", kinds["testLogin"])
1733+
}
1734+
}
1735+
16841736
func TestParsePython_TestFunction(t *testing.T) {
16851737
src := `def test_add():
16861738
assert 1 + 1 == 2

0 commit comments

Comments
 (0)