From 00258aa48423bc0c51f222a31fd339290278a2fe Mon Sep 17 00:00:00 2001 From: "Tapabrata \"Topo\" Pal" Date: Thu, 15 Oct 2026 07:07:00 -0400 Subject: [PATCH 1/2] csharp: typed constant arguments and #if/#else using selection Argument tokens: suffixed and negated literals (-0.0d, 9.0m, 1u, 1UL), BCL static values (DateTime.Now, Int32.MaxValue, double.NaN, Guid.Empty, string.Empty), `x as T`, comparisons/logical operators and !x as bool, nameof as string. Hex literals no longer read F/D digits as suffixes. Imports: of a #if/#elif/#else group of using directives, only the branch the compiler takes with no symbols defined is recorded (#if NET20 using LinqBridge; #else using System.Linq). A lone #if keeps its using. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KXa6Cv1ZUXcdMqXcJfQFhX --- strategies/cspreproc.go | 202 +++++++++++++++++++++++++++++++++++ strategies/cspreproc_test.go | 52 +++++++++ strategies/metadata.go | 114 ++++++++++++++++++-- strategies/metadata_test.go | 38 +++++++ strategies/registry.go | 4 + 5 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 strategies/cspreproc.go create mode 100644 strategies/cspreproc_test.go diff --git a/strategies/cspreproc.go b/strategies/cspreproc.go new file mode 100644 index 0000000..1d297e7 --- /dev/null +++ b/strategies/cspreproc.go @@ -0,0 +1,202 @@ +package strategies + +import ( + "strings" +) + +// csAlternateInactiveLines returns the 1-based lines that sit in a losing +// branch of a C# `#if ... #elif/#else ... #endif` group, evaluated with no +// conditional symbols defined (what the compiler sees without +// DefineConstants). Only groups with an alternative are decided: a file +// that writes +// +// #if NET20 +// using Newtonsoft.Json.Utilities.LinqBridge; +// #else +// using System.Linq; +// #endif +// +// imports System.Linq, not both. A lone `#if X` with no #else stays live: +// without the build's symbols, keeping it is the conservative reading. +func csAlternateInactiveLines(src []byte) map[int]bool { + if !strings.Contains(string(src), "#else") && !strings.Contains(string(src), "#elif") { + return nil + } + type branch struct { + start, end int // lines strictly inside the branch + cond string + isElse bool + } + type group struct{ branches []branch } + var stack []*group + var groups []*group + lines := strings.Split(string(src), "\n") + for i, raw := range lines { + ln := i + 1 + t := strings.TrimSpace(raw) + if !strings.HasPrefix(t, "#") { + continue + } + t = strings.TrimSpace(t[1:]) + word, rest, _ := strings.Cut(t, " ") + rest = strings.TrimSpace(rest) + if j := strings.Index(rest, "//"); j >= 0 { + rest = strings.TrimSpace(rest[:j]) + } + switch word { + case "if": + g := &group{branches: []branch{{start: ln + 1, cond: rest}}} + stack = append(stack, g) + groups = append(groups, g) + case "elif", "else": + if len(stack) == 0 { + continue + } + g := stack[len(stack)-1] + g.branches[len(g.branches)-1].end = ln - 1 + g.branches = append(g.branches, branch{start: ln + 1, cond: rest, isElse: word == "else"}) + case "endif": + if len(stack) == 0 { + continue + } + g := stack[len(stack)-1] + g.branches[len(g.branches)-1].end = ln - 1 + stack = stack[:len(stack)-1] + } + } + out := map[int]bool{} + for _, g := range groups { + if len(g.branches) < 2 || g.branches[len(g.branches)-1].end == 0 { + continue // no alternative, or unterminated + } + active := -1 + for i, b := range g.branches { + if b.isElse { + active = i + break + } + v, ok := csEvalCondition(b.cond) + if !ok { + active = -2 // an expression we cannot read: decide nothing + break + } + if v { + active = i + break + } + } + if active == -2 { + continue + } + for i, b := range g.branches { + if i == active { + continue + } + for ln := b.start; ln <= b.end; ln++ { + out[ln] = true + } + } + } + return out +} + +// csEvalCondition evaluates a C# preprocessor expression with every symbol +// undefined: identifiers are false, true/false are literals, and !, &&, ||, +// ==, != and parentheses combine them. ok is false on anything else. +func csEvalCondition(expr string) (val, ok bool) { + var toks []string + for i := 0; i < len(expr); { + c := expr[i] + switch { + case c == ' ' || c == '\t' || c == '\r': + i++ + case strings.HasPrefix(expr[i:], "&&"), strings.HasPrefix(expr[i:], "||"), + strings.HasPrefix(expr[i:], "=="), strings.HasPrefix(expr[i:], "!="): + toks = append(toks, expr[i:i+2]) + i += 2 + case c == '!' || c == '(' || c == ')': + toks = append(toks, string(c)) + i++ + case c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z': + j := i + for j < len(expr) && (expr[j] == '_' || expr[j] >= 'a' && expr[j] <= 'z' || expr[j] >= 'A' && expr[j] <= 'Z' || expr[j] >= '0' && expr[j] <= '9') { + j++ + } + toks = append(toks, expr[i:j]) + i = j + default: + return false, false + } + } + p := &csCondParser{toks: toks} + v := p.or() + if p.bad || p.pos != len(toks) { + return false, false + } + return v, true +} + +type csCondParser struct { + toks []string + pos int + bad bool +} + +func (p *csCondParser) peek() string { + if p.pos < len(p.toks) { + return p.toks[p.pos] + } + return "" +} + +func (p *csCondParser) prim() bool { + t := p.peek() + p.pos++ + switch t { + case "!": + return !p.prim() + case "(": + v := p.or() + if p.peek() != ")" { + p.bad = true + } + p.pos++ + return v + case "true": + return true + case "", "&&", "||", "==", "!=", ")": + p.bad = true + } + return false // `false` or an undefined symbol +} + +func (p *csCondParser) eq() bool { + v := p.prim() + for p.peek() == "==" || p.peek() == "!=" { + op := p.peek() + p.pos++ + w := p.prim() + v = (v == w) == (op == "==") + } + return v +} + +func (p *csCondParser) and() bool { + v := p.eq() + for p.peek() == "&&" { + p.pos++ + w := p.eq() + v = v && w + } + return v +} + +func (p *csCondParser) or() bool { + v := p.and() + for p.peek() == "||" { + p.pos++ + w := p.and() + v = v || w + } + return v +} diff --git a/strategies/cspreproc_test.go b/strategies/cspreproc_test.go new file mode 100644 index 0000000..a1381c8 --- /dev/null +++ b/strategies/cspreproc_test.go @@ -0,0 +1,52 @@ +package strategies + +import ( + "strings" + "testing" +) + +func TestCSEvalCondition(t *testing.T) { + for expr, want := range map[string]bool{ + "NET20": false, "!HAVE_LINQ": true, "!(NET20 || NET35)": true, + "!(PORTABLE || DNXCORE50) || NETSTANDARD2_0": true, "true && !X": true, + "A == B": true, "A != B": false, "false": false, + } { + got, ok := csEvalCondition(expr) + if !ok || got != want { + t.Errorf("%q = %v,%v want %v", expr, got, ok, want) + } + } + if _, ok := csEvalCondition("A &&"); ok { + t.Errorf("malformed expression must not evaluate") + } +} + +// Only the branch the compiler takes with no symbols defined imports: the +// #else's System.Linq, not the NET20 LinqBridge polyfill. A lone #if keeps +// its using. +func TestCSharpImportsSkipLosingAlternateBranch(t *testing.T) { + src := `#if NET20 +using Newtonsoft.Json.Utilities.LinqBridge; +#else +using System.Linq; +#endif +#if !HAVE_LINQ +using Polyfill.A; +#else +using Polyfill.B; +#endif +#if !(NET20 || NET35) +using System.Numerics; +#endif +class C {}` + inactive := csAlternateInactiveLines([]byte(src)) + var live []string + for i, l := range strings.Split(src, "\n") { + if strings.HasPrefix(l, "using ") && !inactive[i+1] { + live = append(live, strings.TrimSuffix(strings.TrimPrefix(l, "using "), ";")) + } + } + if got := strings.Join(live, ","); got != "System.Linq,Polyfill.A,System.Numerics" { + t.Errorf("live usings = %s", got) + } +} diff --git a/strategies/metadata.go b/strategies/metadata.go index a11cc59..17e48b7 100644 --- a/strategies/metadata.go +++ b/strategies/metadata.go @@ -206,7 +206,17 @@ func argToken(c *sitter.Node, src []byte) string { case "decimal_integer_literal", "hex_integer_literal", "octal_integer_literal", "binary_integer_literal", "int_literal", "integer", "integer_literal", "number_literal": text := string(c.Content(src)) + lower := strings.ToLower(text) + hex := strings.HasPrefix(lower, "0x") switch { + case strings.HasSuffix(lower, "ul") || strings.HasSuffix(lower, "lu"): + return "#ulong" // C# + case strings.HasSuffix(lower, "u") && c.Type() == "integer_literal": + return "#uint" // C# + case strings.HasSuffix(lower, "m") && c.Type() == "integer_literal": + return "#decimal" // C# + case hex && !strings.HasSuffix(lower, "l"): + return "#int" // 0x1F: F and D are hex digits, not suffixes case strings.HasSuffix(text, "L") || strings.HasSuffix(text, "l"): return "#long" case strings.HasSuffix(text, "F") || strings.HasSuffix(text, "f"): @@ -222,7 +232,31 @@ func argToken(c *sitter.Node, src []byte) string { if strings.HasSuffix(text, "F") || strings.HasSuffix(text, "f") { return "#float" } + if c.Type() == "real_literal" && (strings.HasSuffix(text, "M") || strings.HasSuffix(text, "m")) { + return "#decimal" // C# 9.0m + } return "#double" + case "prefix_unary_expression", "unary_expression": + // -1L, -0.0d, +x, ~mask keep the operand's type; !x is a bool. + text := strings.TrimSpace(string(c.Content(src))) + if strings.HasPrefix(text, "!") && !strings.HasPrefix(text, "!=") { + return "#boolean" + } + if (strings.HasPrefix(text, "-") || strings.HasPrefix(text, "+") || strings.HasPrefix(text, "~")) && + !strings.HasPrefix(text, "--") && !strings.HasPrefix(text, "++") && c.NamedChildCount() == 1 { + if v := argToken(c.NamedChild(0), src); strings.HasPrefix(v, "#") { + return v + } + } + case "as_expression": + // C#: `o as Uri` is typed Uri (or null). + if t := c.ChildByFieldName("right"); t != nil { + if name := csTypeLastName(t, src); name != "" { + return "#" + name + } + } + case "is_expression", "is_pattern_expression": + return "#boolean" case "true", "false": return "#boolean" case "cast_expression": @@ -272,11 +306,8 @@ func argToken(c *sitter.Node, src []byte) string { if name != nil && obj != nil { n := string(name.Content(src)) t := string(obj.Content(src)) - if n == "MaxValue" || n == "MinValue" { - switch t { - case "int", "long", "short", "byte", "char", "double", "float", "decimal", "uint", "ulong", "ushort", "sbyte": - return "#" + t - } + if typ := csStaticValueType(t, n); typ != "" { + return "#" + typ } // Formatting.Indented: a PascalCase member of a PascalCase // identifier is an enum value (or a static member) — "%Type" @@ -316,8 +347,18 @@ func argToken(c *sitter.Node, src []byte) string { return argToken(c.NamedChild(0), src) } case "binary_expression": + op := c.ChildByFieldName("operator") + if op != nil && c.ChildByFieldName("left") != nil { + switch string(op.Content(src)) { + case "==", "!=", "<", ">", "<=", ">=", "&&", "||": + // Comparisons and logical operators are bool in every + // C-family language here (C# overloads `operator ==` only + // to return bool by convention; tree-sitter C# names it). + return "#boolean" + } + } // C#: `"#" + hex` is a string whenever either operand is one. - if op := c.ChildByFieldName("operator"); op != nil && string(op.Content(src)) == "+" { + if op != nil && string(op.Content(src)) == "+" { l, r := c.ChildByFieldName("left"), c.ChildByFieldName("right") if l != nil && argToken(l, src) == "#String" || r != nil && argToken(r, src) == "#String" { return "#String" @@ -350,7 +391,7 @@ func argToken(c *sitter.Node, src []byte) string { name = string(fn.NamedChild(0).Content(src)) } } - if name == "ToString" { + if name == "ToString" || name == "nameof" && fn.Type() == "identifier" { return "#String" } if name != "" { @@ -2054,3 +2095,62 @@ func rustExprReceiver(value *sitter.Node, src []byte) string { // call on a value of unknown type — never a bare call to a function. return rustExprReceiverMark } + +// csStaticValueType types a C# BCL static value by its declaring type: +// DateTime.Now, Int32.MaxValue, double.NaN, Guid.Empty, string.Empty. +// Only well-known members whose type is the declaring type itself (or +// string for String.Empty); anything else is "". +func csStaticValueType(typ, member string) string { + kw := map[string]string{ + "Int16": "short", "Int32": "int", "Int64": "long", "UInt16": "ushort", "UInt32": "uint", "UInt64": "ulong", + "Byte": "byte", "SByte": "sbyte", "Single": "float", "Double": "double", "Decimal": "decimal", "Char": "char", + "short": "short", "int": "int", "long": "long", "ushort": "ushort", "uint": "uint", "ulong": "ulong", + "byte": "byte", "sbyte": "sbyte", "float": "float", "double": "double", "decimal": "decimal", "char": "char", + } + if k, ok := kw[typ]; ok { + switch member { + case "MaxValue", "MinValue": + return k + case "NaN", "PositiveInfinity", "NegativeInfinity", "Epsilon": + if k == "double" || k == "float" { + return k + } + case "One", "Zero", "MinusOne": + if k == "decimal" { + return k + } + } + return "" + } + switch typ { + case "String", "string": + if member == "Empty" { + return "String" + } + case "DateTime": + switch member { + case "Now", "UtcNow", "Today", "MinValue", "MaxValue", "UnixEpoch": + return typ + } + case "DateTimeOffset": + switch member { + case "Now", "UtcNow", "MinValue", "MaxValue", "UnixEpoch": + return typ + } + case "TimeSpan": + switch member { + case "Zero", "MinValue", "MaxValue": + return typ + } + case "Guid": + if member == "Empty" { + return typ + } + case "BigInteger": + switch member { + case "One", "Zero", "MinusOne": + return typ + } + } + return "" +} diff --git a/strategies/metadata_test.go b/strategies/metadata_test.go index 4739d8e..db8dcad 100644 --- a/strategies/metadata_test.go +++ b/strategies/metadata_test.go @@ -639,6 +639,44 @@ func TestCSharp_CallSiteArgMarkers(t *testing.T) { } } +// Typed constants and operators the Roslyn overload choice depends on: +// suffixed and negated literals, BCL static values, `as`, comparisons and +// nameof (newtonsoft JValue/WriteValue overloads, oracle review 2026-10-16). +func TestCSharp_CallSiteTypedConstantArgs(t *testing.T) { + src := `class A { + void F(object o) { + W(-0.0d); + W(9.0m); + W(1u); + W(1UL); + W(-5L); + W(DateTime.Now); + W(Int32.MaxValue); + W(double.NaN); + W(Guid.Empty); + W(string.Empty); + W(TimeSpan.Zero); + W(o as Uri); + W(a == b); + W(!ok); + W(nameof(F)); + } +}` + syms, _ := extract(t, astkit.LangCSharp, src) + var got []string + for _, s := range syms { + for _, cs := range s.CallSites { + if cs.Callee == "W" { + got = append(got, strings.Join(cs.Args, ",")) + } + } + } + want := []string{"#double", "#decimal", "#uint", "#ulong", "#long", "#DateTime", "#int", "#double", "#Guid", "#String", "#TimeSpan", "#Uri", "#boolean", "#boolean", "#String"} + if strings.Join(got, " ") != strings.Join(want, " ") { + t.Errorf("args\n got %v\nwant %v", got, want) + } +} + // A C prototype for a function defined in the same file is not a second // function; a prototype without a definition (a header) stays. func TestC_PrototypeCollapsesIntoDefinition(t *testing.T) { diff --git a/strategies/registry.go b/strategies/registry.go index 83d4c7a..e1b340d 100644 --- a/strategies/registry.go +++ b/strategies/registry.go @@ -467,10 +467,14 @@ func (c *csharpStrategy) ExtractImports(tree *sitter.Tree, src []byte) ([]astkit // whole test files in `#if !(NET20 || ...)`) and inside namespace // blocks; walk the tree, not just the root's children, or such a // file reports no imports at all. + inactive := csAlternateInactiveLines(src) internalast.WalkTree(tree.RootNode(), func(n *sitter.Node) { if n == nil || n.Type() != "using_directive" { return } + if inactive[int(n.StartPoint().Row)+1] { + return + } raw := strings.TrimSpace(internalast.NodeText(n, src)) path := strings.TrimSpace(strings.TrimSuffix(raw, ";")) path = strings.TrimSpace(strings.TrimPrefix(path, "global ")) From 73b7b51a05ebe3ec4851b7d873f2b6be827f59d9 Mon Sep 17 00:00:00 2001 From: "Tapabrata \"Topo\" Pal" Date: Thu, 15 Oct 2026 07:17:48 -0400 Subject: [PATCH 2/2] js/ts: a function declared inside a function is its own local symbol Nested function declarations (socket.io parser-v3's encodeOne inside encodePayload) were not indexed and their calls were credited to the enclosing function. They are now function symbols marked "local", qualified by their enclosing declarations like the TypeScript pass names them (encodePayload.encodeOne, Socket.onClose.cleanup), and the enclosing function's call sites stop at them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KXa6Cv1ZUXcdMqXcJfQFhX --- strategies/extractors.go | 84 +++++++++++++++++++++++++++++++++++++- strategies/jslocal_test.go | 47 +++++++++++++++++++++ strategies/metadata.go | 7 ++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 strategies/jslocal_test.go diff --git a/strategies/extractors.go b/strategies/extractors.go index e5aa355..10b615f 100644 --- a/strategies/extractors.go +++ b/strategies/extractors.go @@ -369,6 +369,7 @@ func goIdentifierNames(n *sitter.Node, src []byte) []string { func extractJSNodes(root *sitter.Node, filePath, blobSHA, language string, src []byte, imports []string) []astkit.Symbol { var out []astkit.Symbol jsVisit(root, filePath, blobSHA, language, src, imports, "", false, &out) + jsLocalFunctions(root, filePath, blobSHA, language, src, imports, &out) out = jsDedupeThisFields(out) for name := range jsNamedExports(root, src) { for i := range out { @@ -2950,7 +2951,9 @@ func cMacroSymbol(name string, paramsNode *sitter.Node, body, raw string, span a // (`log:message:`) or one unary name. func objcMacroSends(code string, line int) []astkit.CallSite { var out []astkit.CallSite - isIdent := func(b byte) bool { return b == '_' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' } + isIdent := func(b byte) bool { + return b == '_' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' + } for i := 0; i < len(code); i++ { if code[i] != '[' { continue @@ -6860,3 +6863,82 @@ func funcSig(n *sitter.Node, src []byte) string { sig = strings.TrimRight(sig, " \t\n{") return strings.TrimSpace(sig) } + +// jsFunctionLike reports the JS/TS nodes that open a function body. +func jsFunctionLike(t string) bool { + switch t { + case "function_declaration", "generator_function_declaration", "function_expression", "function", + "generator_function", "arrow_function", "method_definition", "class_static_block": + return true + } + return false +} + +// jsNestedFunctionDecl reports a function declaration written inside a +// function body (`function encodeOne(packet) {...}` inside encodePayload): +// it is a symbol of its own, so its calls are not its encloser's. +func jsNestedFunctionDecl(n *sitter.Node) bool { + if n.Type() != "function_declaration" && n.Type() != "generator_function_declaration" { + return false + } + for p := n.Parent(); p != nil; p = p.Parent() { + if jsFunctionLike(p.Type()) { + return true + } + } + return false +} + +// jsLocalFunctions emits every nested function declaration as a function +// symbol marked "local": visible only inside the function that declares it +// (consumers scope bare calls by that). socket.io's parser-v3 encodeOne +// was unindexed and its calls were credited to encodePayload. +func jsLocalFunctions(root *sitter.Node, filePath, blobSHA, language string, src []byte, imports []string, out *[]astkit.Symbol) { + var walk func(n *sitter.Node) + walk = func(n *sitter.Node) { + if n == nil { + return + } + if jsNestedFunctionDecl(n) { + if sym := jsNamedSym(n, "name", filePath, blobSHA, language, src, imports, astkit.KindFunction, "", false); sym != nil { + // Named like the TypeScript pass's declaration of it: + // qualified by the enclosing declarations, no parent type. + sym.QualifiedName = qualJoin(jsEnclosingQualifier(n, src), sym.Name) + sym.Modifiers = append(sym.Modifiers, "local") + *out = append(*out, *sym) + } + } + for i := 0; i < int(n.ChildCount()); i++ { + walk(n.Child(i)) + } + } + walk(root) +} + +// jsEnclosingQualifier names the declarations enclosing n, outermost first: +// functions, classes, methods and `const f = () => ...` bindings +// ("Socket.onClose" for a function declared inside that method). Anonymous +// enclosers add nothing. +func jsEnclosingQualifier(n *sitter.Node, src []byte) string { + var parts []string + for p := n.Parent(); p != nil; p = p.Parent() { + name := "" + switch p.Type() { + case "function_declaration", "generator_function_declaration", "class_declaration", "abstract_class_declaration", + "method_definition", "internal_module", "module": + if nn := p.ChildByFieldName("name"); nn != nil { + name = nn.Content(src) + } + case "arrow_function", "function_expression", "function", "generator_function": + if d := p.Parent(); d != nil && d.Type() == "variable_declarator" { + if nn := d.ChildByFieldName("name"); nn != nil && nn.Type() == "identifier" { + name = nn.Content(src) + } + } + } + if name != "" { + parts = append([]string{name}, parts...) + } + } + return strings.Join(parts, ".") +} diff --git a/strategies/jslocal_test.go b/strategies/jslocal_test.go new file mode 100644 index 0000000..c3fe29c --- /dev/null +++ b/strategies/jslocal_test.go @@ -0,0 +1,47 @@ +package strategies_test + +import ( + "testing" + + "github.com/provasign/astkit" +) + +// A function declared inside a function body is its own symbol, marked +// local and qualified by its enclosers; its calls are not its encloser's. +func TestJS_NestedFunctionDeclarationIsLocalSymbol(t *testing.T) { + src := `export function encodePayload(packets, callback) { + function encodeOne(packet, done) { + encodePacket(packet, done); + } + map(packets, encodeOne, callback); +} +class Socket { + onClose() { + function cleanup() { clearTimeout(this.t); } + cleanup(); + } +}` + for _, lang := range []astkit.LanguageKey{astkit.LangTypeScript, astkit.LangJavaScript} { + syms, _ := extract(t, lang, src) + byQual := map[string]astkit.Symbol{} + for _, s := range syms { + byQual[s.QualifiedName] = s + } + one, ok := byQual["encodePayload.encodeOne"] + if !ok || one.ParentName != "" || !contains(one.Modifiers, "local") { + t.Fatalf("%s: encodeOne = %+v (symbols %v)", lang, one, byQual) + } + if len(one.CallSites) != 1 || one.CallSites[0].Callee != "encodePacket" { + t.Errorf("%s: encodeOne calls %v", lang, one.CallSites) + } + for _, cs := range byQual["encodePayload"].CallSites { + if cs.Callee == "encodePacket" { + t.Errorf("%s: encodePayload must not own encodeOne's call", lang) + } + } + if _, ok := byQual["Socket.onClose.cleanup"]; !ok { + t.Errorf("%s: nested function in a method missing: %v", lang, byQual) + } + } +} + diff --git a/strategies/metadata.go b/strategies/metadata.go index 17e48b7..c029b51 100644 --- a/strategies/metadata.go +++ b/strategies/metadata.go @@ -26,6 +26,9 @@ type callSpec struct { // genericFn reports whether a call carries explicit type arguments // (DeserializeObject(...)); nil means "never generic" for the language. genericFn func(call *sitter.Node, src []byte) bool + // skip reports a nested node whose calls belong to another symbol (a + // JS/TS function declaration inside a function body is its own symbol). + skip func(n *sitter.Node) bool } // qualifierName reduces a callee's receiver operand to one identifier so @@ -100,6 +103,9 @@ func collectCallSites(body *sitter.Node, src []byte, spec callSpec) []astkit.Cal if n == nil { return } + if n != body && spec.skip != nil && spec.skip(n) { + return + } for _, t := range spec.nodeTypes { if n.Type() == t { callee := spec.calleeFn(n, src) @@ -511,6 +517,7 @@ func goCallSites(body *sitter.Node, src []byte) []astkit.CallSite { func jsCallSites(body *sitter.Node, src []byte) []astkit.CallSite { return collectCallSites(body, src, callSpec{ nodeTypes: []string{"call_expression", "new_expression", "jsx_opening_element", "jsx_self_closing_element"}, + skip: jsNestedFunctionDecl, calleeFn: func(call *sitter.Node, src []byte) string { if call.Type() == "jsx_opening_element" || call.Type() == "jsx_self_closing_element" { if name := call.ChildByFieldName("name"); name != nil {