Skip to content

Commit

Permalink
Improve finding symbols in a SelectorExpr.
Browse files Browse the repository at this point in the history
Improve its tests.
  • Loading branch information
pherrymason committed Jan 17, 2025
1 parent cdd3244 commit 2374317
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 25 deletions.
80 changes: 56 additions & 24 deletions server/internal/lsp/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,45 @@ func FindSymbolAtPosition(pos lsp.Position, fileName string, symbolTable SymbolT
// If parent is a SelectExpr, we will need to first search chain of elements to be able to find `name`.

totalSteps := len(path)
selectExpr := false
//identFound := false
parentNodeIsSelectorExpr := false
var parentSelectorExpr *ast.SelectorExpr

index := 0
selectorsChained := 0
for i := totalSteps - 1; i >= 0; i-- {
switch path[i].node.(type) {
case *ast.Ident, ast.Ident:
//identFound = true

case *ast.SelectorExpr:
selectExpr = true
index = i
i = 0
selectorsChained++
parentSelectorExpr = path[i].node.(*ast.SelectorExpr)
if !parentNodeIsSelectorExpr {
parentNodeIsSelectorExpr = true
index = i
}

default:
if parentNodeIsSelectorExpr {
i = 0
}
}
}

if selectExpr {
if path[index+1].propertyName == "Sel" {
if parentNodeIsSelectorExpr {
step := path[index+1]
if step.propertyName == "Sel" {
if selectorsChained > 1 {
// Even if we are resolving final part of a SelectorExpr, we are in the middle of a bigger chain of SelectorExpr. This means
}

// We need to solve first SelectorExpr.X!
symbol := solveSelAtSelectorExpr(
path[index].node.(*ast.SelectorExpr),
pos,
fileName,
moduleName,
symbolTable,
0,
)

if symbol != nil {
Expand All @@ -101,27 +116,33 @@ func FindSymbolAtPosition(pos lsp.Position, fileName string, symbolTable SymbolT
// As cursor is at X, we can just search normally.
}
}

if parentNodeIsSelectorExpr {
parentSelectorExpr.StartPosition()
parentSelectorExpr = nil
}
// -------------------------------------------------

// Normal search
sym := symbolTable.FindSymbolByPosition(pos, fileName, name, moduleName, 0)

return sym
}

// solveSelAtSelectorExpr solves iteratively the X part of SelectorExpr
// Solves X. If X is itself a SelectorExpr, it will follow the chain and solve the symbol just before the last '.'
func solveSelAtSelectorExpr(selectorExpr *ast.SelectorExpr, pos lsp.Position, fileName string, moduleName ModuleName, symbolTable SymbolTable) *Symbol {
// solveSelAtSelectorExpr resolves Sel Ident symbol.
func solveSelAtSelectorExpr(selectorExpr *ast.SelectorExpr, pos lsp.Position, fileName string, moduleName ModuleName, symbolTable SymbolTable, deepLevel uint) *Symbol {
// To be able to resolve selectorExpr.Sel, we need to know first what is selectorExpr.X is or what does it return.
var parentSymbol *Symbol
switch base := selectorExpr.X.(type) {
case *ast.Ident:
// X is a plain Ident. We need to resolve Ident Type:
// - Ident might be a variable. What's its type? Struct/Enum/Fault?
parentSymbol = symbolTable.SolveType(base.Name, pos, fileName)
if parentSymbol == nil {
return nil
}

case *ast.SelectorExpr:
parentSymbol = solveSelAtSelectorExpr(base, pos, fileName, moduleName, symbolTable)
// X is a SelectorExpr itself, we need to solve the type of base.Sel
parentSymbol = solveSelAtSelectorExpr(base, pos, fileName, moduleName, symbolTable, deepLevel+1)
if parentSymbol == nil {
return nil
}
Expand All @@ -130,7 +151,7 @@ func solveSelAtSelectorExpr(selectorExpr *ast.SelectorExpr, pos lsp.Position, fi
ident := base.Identifier
switch i := ident.(type) {
case *ast.SelectorExpr:
parentSymbol = solveSelAtSelectorExpr(i, pos, fileName, moduleName, symbolTable)
parentSymbol = solveSelAtSelectorExpr(i, pos, fileName, moduleName, symbolTable, deepLevel+1)
if parentSymbol == nil {
return nil
}
Expand All @@ -146,21 +167,25 @@ func solveSelAtSelectorExpr(selectorExpr *ast.SelectorExpr, pos lsp.Position, fi
return nil
}

return solveSymbolChild(parentSymbol, selectorExpr.Sel.Name, moduleName, fileName, &symbolTable)
// We've found X type, we are ready to find selectorExpr.Sel inside `X`'s type:
solveElementType := true
if deepLevel == 0 {
solveElementType = false
}
return resolveChildSymbol(parentSymbol, selectorExpr.Sel.Name, moduleName, fileName, &symbolTable, solveElementType)
}

func solveSymbolChild(symbol *Symbol, childName string, moduleName ModuleName, fileName string, symbolTable *SymbolTable) *Symbol {
func resolveChildSymbol(symbol *Symbol, nextIdent string, moduleName ModuleName, fileName string, symbolTable *SymbolTable, solveType bool) *Symbol {
if symbol == nil {
return nil
}

selIdent := childName
switch symbol.Kind {
case ast.STRUCT:
// Search In Members
for _, member := range symbol.NodeDecl.(*ast.StructDecl).Members {
if member.Names[0].Name == selIdent {
if member.Type.BuiltIn {
if member.Names[0].Name == nextIdent {
if member.Type.BuiltIn || !solveType {
return &Symbol{
Name: member.Names[0].Name,
Module: moduleName,
Expand All @@ -176,20 +201,26 @@ func solveSymbolChild(symbol *Symbol, childName string, moduleName ModuleName, f
}
}

// If nextIdent is the last element in the chain of SelectorExpr, we don't need to resolve the type.
// Else, we need to check for the type to continue resolving each step of the chain
value := symbolTable.FindSymbolByPosition(
member.Range.Start,
fileName,
member.Type.Identifier.Name,
moduleName,
0,
)
return value.Get()
if value.IsSome() {
return value.Get()
} else {
return nil
}
}
}

// Not found in members, we need to search struct methods
for _, relatedSymbol := range symbol.Children {
if relatedSymbol.Tag == Method && relatedSymbol.Child.Name == selIdent {
if relatedSymbol.Tag == Method && relatedSymbol.Child.Name == nextIdent {
return relatedSymbol.Child
}
}
Expand All @@ -206,12 +237,13 @@ func solveSymbolChild(symbol *Symbol, childName string, moduleName ModuleName, f
)

if returnTypeSymbol.IsSome() {
return solveSymbolChild(
return resolveChildSymbol(
returnTypeSymbol.Get(),
selIdent,
nextIdent,
moduleName,
fileName,
symbolTable,
solveType,
)
}
}
Expand Down
71 changes: 70 additions & 1 deletion server/internal/lsp/analysis/analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,19 @@ func TestFindsSymbol_Declaration_struct(t *testing.T) {
t.Run("Find struct field definition in same module", func(t *testing.T) {
source := `struct Animal {
int life;
Taxonomy taxId;
}
fn void main(){
Animal dog;
dog.life = 3;
dog.taxId = 1;
}`

fileName := "app.c3"
tree := getTree(source, fileName)
symbolTable := BuildSymbolTable(tree, fileName)

cursorPosition := lsp.Position{Line: 5, Column: 8}
cursorPosition := lsp.Position{Line: 6, Column: 8}
symbolOpt := FindSymbolAtPosition(cursorPosition, fileName, symbolTable, tree)
assert.True(t, symbolOpt.IsSome())
symbol := symbolOpt.Get()
Expand All @@ -247,6 +249,33 @@ func TestFindsSymbol_Declaration_struct(t *testing.T) {
assert.Equal(t, lsp.NewRange(1, 3, 1, 12), symbol.NodeDecl.GetRange())
assert.Equal(t, "int", symbol.Type.Name)
assert.Equal(t, ast.Token(ast.FIELD), symbol.Kind)

// -----------------------------------------------
// Following is testing finding the symbol when cursor is in property that is not a builtin type
// -----------------------------------------------
cursorPosition = lsp.Position{Line: 7, Column: 8}
symbolOpt = FindSymbolAtPosition(cursorPosition, fileName, symbolTable, tree)
assert.True(t, symbolOpt.IsSome())
symbol = symbolOpt.Get()
assert.Equal(t, "taxId", symbol.Name)
assert.Equal(t, ModuleName("app"), symbol.Module)
assert.Equal(t, lsp.NewRange(2, 3, 2, 18), symbol.NodeDecl.GetRange())
assert.Equal(t, "Taxonomy", symbol.Type.Name)
assert.Equal(t, ast.Token(ast.FIELD), symbol.Kind)

//-----------------------------------------------
// Following is testing finding the symbol when cursor is in Selector.Expr
// -----------------------------------------------
cursorPosition = lsp.Position{Line: 6, Column: 4} // Cursor at d|og.life
symbolOpt = FindSymbolAtPosition(cursorPosition, fileName, symbolTable, tree)

assert.True(t, symbolOpt.IsSome())
symbol = symbolOpt.Get()
assert.Equal(t, "dog", symbol.Name)
assert.Equal(t, ModuleName("app"), symbol.Module)
assert.Equal(t, lsp.NewRange(5, 3, 5, 14), symbol.NodeDecl.GetRange())
assert.Equal(t, "Animal", symbol.Type.Name)
assert.Equal(t, ast.Token(ast.VAR), symbol.Kind)
})

t.Run("Find struct method definition in same module", func(t *testing.T) {
Expand Down Expand Up @@ -298,7 +327,21 @@ func TestFindsSymbol_Declaration_struct(t *testing.T) {
assert.Equal(t, lsp.NewRange(2, 3, 2, 12), symbol.NodeDecl.GetRange())
assert.Equal(t, "int", symbol.Type.Name)
assert.Equal(t, ast.Token(ast.FIELD), symbol.Kind)

// -----------------------------------------------
// Following is testing finding the symbol when cursor is in Selector.Expr
// -----------------------------------------------
cursorPosition = lsp.Position{Line: 10, Column: 8} // Cursor is at dog.b|eing.life
symbolOpt = FindSymbolAtPosition(cursorPosition, fileName, symbolTable, tree)
assert.True(t, symbolOpt.IsSome())
symbol = symbolOpt.Get()
assert.Equal(t, "being", symbol.Name)
assert.Equal(t, ModuleName("app"), symbol.Module)
assert.Equal(t, lsp.NewRange(6, 3, 6, 15), symbol.NodeDecl.GetRange())
assert.Equal(t, "Being", symbol.Type.Name)
assert.Equal(t, ast.Token(ast.FIELD), symbol.Kind)
})

t.Run("Find indirect struct field in a method chain in same module", func(t *testing.T) {
source := `
struct Sound {
Expand Down Expand Up @@ -413,4 +456,30 @@ func TestFindsSymbol_Declaration_function(t *testing.T) {
assert.Equal(t, lsp.NewRange(1, 1, 1, 79), symbol.NodeDecl.GetRange())
assert.Equal(t, ast.Token(ast.FUNCTION), symbol.Kind)
})

// This test is interesting because we are playing with the cursor placed in SelectorExpr.X
t.Run("Find function returning a struct", func(t *testing.T) {
source := `
struct Sound {
int length;
}
fn Sound bark() {}
fn void main(){
Animal dog;
bark().length = 3;
}`

fileName := "app.c3"
tree := getTree(source, fileName)
symbolTable := BuildSymbolTable(tree, fileName)

cursorPosition := lsp.Position{Line: 7, Column: 4}
symbolOpt := FindSymbolAtPosition(cursorPosition, fileName, symbolTable, tree)
assert.True(t, symbolOpt.IsSome())
symbol := symbolOpt.Get()
assert.Equal(t, "bark", symbol.Name)
assert.Equal(t, ModuleName("app"), symbol.Module)
assert.Equal(t, lsp.NewRange(4, 2, 4, 20), symbol.NodeDecl.GetRange())
assert.Equal(t, ast.Token(ast.FUNCTION), symbol.Kind)
})
}

0 comments on commit 2374317

Please sign in to comment.