Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions strategies/cspreproc.go
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions strategies/cspreproc_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
84 changes: 83 additions & 1 deletion strategies/extractors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, ".")
}
Loading
Loading