Skip to content

Commit

Permalink
Merge pull request #103 from PgBiel/improve-macros
Browse files Browse the repository at this point in the history
Improve macro handling
  • Loading branch information
pherrymason authored Jan 26, 2025
2 parents 116f5fb + 9faa5ea commit e358da4
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 63 deletions.
173 changes: 126 additions & 47 deletions server/pkg/parser/node_to_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package parser
import (
"errors"
"fmt"

"github.com/pherrymason/c3-lsp/pkg/cast"
idx "github.com/pherrymason/c3-lsp/pkg/symbols"
sitter "github.com/smacker/go-tree-sitter"
protocol "github.com/tliron/glsp/protocol_3_16"
Expand All @@ -25,9 +27,14 @@ import (
func (p *Parser) nodeToFunction(node *sitter.Node, currentModule *idx.Module, docId *string, sourceCode []byte) (idx.Function, error) {
var typeIdentifier string
funcHeader := node.Child(1)

if funcHeader == nil {
return idx.Function{}, errors.New("child node not found")
}

nameNode := funcHeader.ChildByFieldName("name")

if nameNode == nil || funcHeader == nil {
if nameNode == nil {
return idx.Function{}, errors.New("child node not found")
}

Expand Down Expand Up @@ -162,42 +169,66 @@ func (p *Parser) nodeToArgument(argNode *sitter.Node, methodIdentifier string, c
}

/*
func_definition: $ => seq(
'fn',
$.func_header,
$.fn_parameter_list,
optional($.attributes),
field('body', $.macro_func_body),
trailing_block_param: $ => seq(
$.at_ident,
optional($.fn_parameter_list),
),
func_header: $ => seq(
field('return_type', $._type_or_optional_type),
optional(seq(field('method_type', $.type), '.')),
field('name', $._func_macro_name),
macro_parameter_list: $ => seq(
'(',
optional(
choice(
$._parameters,
seq(
optional($._parameters),
';',
$.trailing_block_param,
),
),
),
')',
),
macro_declaration: $ => seq(
'macro',
choice($.func_header, $.macro_header),
$.macro_parameter_list,
optional($.attributes),
field('body', $.macro_func_body),
),
macro_header: $ => seq(
optional(seq(field('method_type', $.type), '.')),
field('name', $._func_macro_name),
'macro',
$.macro_header,
$.macro_parameter_list,
optional($.attributes),
field('body', $.macro_func_body),
),
macro_header: $ => seq(
optional(field('return_type', $._type_optional)), // Return type is optional for macros
optional(seq(field('method_type', $.type), '.')),
field('name', $._func_macro_name),
),
*/
func (p *Parser) nodeToMacro(node *sitter.Node, currentModule *idx.Module, docId *string, sourceCode []byte) idx.Function {
var typeIdentifier string
func (p *Parser) nodeToMacro(node *sitter.Node, currentModule *idx.Module, docId *string, sourceCode []byte) (idx.Function, error) {
var nameNode *sitter.Node
funcHeader := node.Child(1)
macroHeader := node.Child(1)

if macroHeader == nil {
return idx.Function{}, errors.New("child node not found")
}

nameNode = funcHeader.ChildByFieldName("name")
/*
if funcHeader.Type() == "func_header" && funcHeader.ChildByFieldName("method_type") != nil {
typeIdentifier = funcHeader.ChildByFieldName("method_type").Content(sourceCode)
}*/
nameNode = macroHeader.ChildByFieldName("name")

if nameNode == nil {
return idx.Function{}, errors.New("child node not found")
}

var typeIdentifier string = ""
var returnType *idx.Type = nil

if macroHeader.Type() == "macro_header" {
methodTypeNode := macroHeader.ChildByFieldName("method_type")
if methodTypeNode != nil {
typeIdentifier = methodTypeNode.Content(sourceCode)
}

returnTypeNode := macroHeader.ChildByFieldName("return_type")
if returnTypeNode != nil {
returnType = cast.ToPtr(p.typeNodeToType(returnTypeNode, currentModule, sourceCode))
}
}

var argumentIds []string
arguments := []*idx.Variable{}
Expand All @@ -206,12 +237,45 @@ func (p *Parser) nodeToMacro(node *sitter.Node, currentModule *idx.Module, docId

if parameters.ChildCount() > 2 {
for i := uint32(0); i < parameters.ChildCount(); i++ {
var argument *idx.Variable
argNode := parameters.Child(int(i))
if argNode.Type() != "parameter" {

// '@body' in macro name(args; @body) { ... }
if argNode.Type() == "trailing_block_param" {
identNode := argNode.Child(0)
identifier := identNode.Content(sourceCode)
idRange := idx.NewRangeFromTreeSitterPositions(identNode.StartPoint(), identNode.EndPoint())

// Get body function signature
// If it's missing, it's just empty args
bodyParams := "()"
if argNode.ChildCount() >= 2 && argNode.Child(1).Type() == "fn_parameter_list" {
// TODO: Maybe we should properly parse the parameters at some point
// For now, simple string manipulation suffices
bodyParams = argNode.Child(1).Content(sourceCode)
}

// '@body' is equivalent to a function
// Use a callback type
argType := idx.NewTypeFromString("fn void"+bodyParams, currentModule.GetModuleString())

variable := idx.NewVariable(
identifier,
argType,
currentModule.GetModuleString(),
*docId,
idRange,
idx.NewRangeFromTreeSitterPositions(argNode.StartPoint(),
argNode.EndPoint()),
)

argument = &variable
} else if argNode.Type() == "parameter" {
argument = p.nodeToArgument(argNode, typeIdentifier, currentModule, docId, sourceCode, parameterIndex)
} else {
continue
}

argument := p.nodeToArgument(argNode, typeIdentifier, currentModule, docId, sourceCode, parameterIndex)
arguments = append(
arguments,
argument,
Expand All @@ -221,27 +285,42 @@ func (p *Parser) nodeToMacro(node *sitter.Node, currentModule *idx.Module, docId
}
}

macroName := "??"
if nameNode != nil {
macroName = nameNode.Content(sourceCode)
}
macroName := nameNode.Content(sourceCode)

symbol := idx.NewMacro(
macroName,
argumentIds,
currentModule.GetModuleString(),
*docId,
idx.NewRangeFromTreeSitterPositions(nameNode.StartPoint(),
nameNode.EndPoint()),
idx.NewRangeFromTreeSitterPositions(node.StartPoint(),
node.EndPoint()),
)
var symbol idx.Function
if typeIdentifier != "" {
symbol = idx.NewTypeMacro(
typeIdentifier,
macroName,
argumentIds,
returnType,
currentModule.GetModuleString(),
*docId,
idx.NewRangeFromTreeSitterPositions(nameNode.StartPoint(),
nameNode.EndPoint()),
idx.NewRangeFromTreeSitterPositions(node.StartPoint(),
node.EndPoint()),
protocol.CompletionItemKindFunction,
)
} else {
symbol = idx.NewMacro(
macroName,
argumentIds,
returnType,
currentModule.GetModuleString(),
*docId,
idx.NewRangeFromTreeSitterPositions(nameNode.StartPoint(),
nameNode.EndPoint()),
idx.NewRangeFromTreeSitterPositions(node.StartPoint(),
node.EndPoint()),
)
}

if node.ChildByFieldName("body") != nil {
variables := p.FindVariableDeclarations(node, currentModule.GetModuleString(), currentModule, docId, sourceCode)
variables = append(arguments, variables...)
symbol.AddVariables(variables)
}

return symbol
return symbol, nil
}
10 changes: 6 additions & 4 deletions server/pkg/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,13 @@ func (p *Parser) ParseSymbols(doc *document.Document) (symbols_table.UnitModules
}

case "macro_declaration":
macro := p.nodeToMacro(c.Node, moduleSymbol, &doc.URI, sourceCode)
moduleSymbol.AddFunction(&macro)
macro, err := p.nodeToMacro(c.Node, moduleSymbol, &doc.URI, sourceCode)
if err == nil {
moduleSymbol.AddFunction(&macro)

if lastDocComment != nil {
macro.SetDocComment(lastDocComment)
if lastDocComment != nil {
macro.SetDocComment(lastDocComment)
}
}
default:
// TODO test that module ends up with wrong endPosition
Expand Down
117 changes: 117 additions & 0 deletions server/pkg/parser/parser_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func TestExtractSymbols_FunctionsWithArguments(t *testing.T) {

fn := symbols.Get("docid").GetChildrenFunctionByName("test")
assert.True(t, fn.IsSome(), "Function was not found")
assert.Equal(t, "fn void test(int number, char ch, int* pointer)", fn.Get().GetHoverInfo(), "Function signature")
assert.Equal(t, "test", fn.Get().GetName(), "Function name")
assert.Equal(t, "void", fn.Get().GetReturnType().GetName(), "Return type")
assert.Equal(t, idx.NewRange(0, 8, 0, 12), fn.Get().GetIdRange())
Expand Down Expand Up @@ -354,6 +355,122 @@ func TestExtractSymbols_StructMemberFunctionWithArguments(t *testing.T) {
})
}

func TestExtractSymbols_StructMemberMacroWithArguments(t *testing.T) {
source := `macro Object* UserStruct.@method(self, int* pointer; @body) {
@body();
return 1;
}`
docId := "docId"
doc := document.NewDocument(docId, source)
parser := createParser()

t.Run("Finds method macro", func(t *testing.T) {
symbols, _ := parser.ParseSymbols(&doc)

fn := symbols.Get("docid").GetChildrenFunctionByName("UserStruct.@method")
assert.True(t, fn.IsSome(), "Method was not found")
assert.Equal(t, "Object", fn.Get().GetReturnType().GetName(), "Return type")
assert.Equal(t, "Object*", fn.Get().GetReturnType().String(), "Return type")
assert.Equal(t, "UserStruct.@method", fn.Get().GetName())
assert.Equal(t, idx.NewRange(0, 25, 0, 32), fn.Get().GetIdRange())
assert.Equal(t, idx.NewRange(0, 0, 3, 2), fn.Get().GetDocumentRange())
})

t.Run("Finds method macro arguments", func(t *testing.T) {
symbols, _ := parser.ParseSymbols(&doc)

fn := symbols.Get("docid").GetChildrenFunctionByName("UserStruct.@method")
assert.True(t, fn.IsSome(), "Method was not found")

assert.Equal(t, "macro Object* UserStruct.@method(UserStruct self, int* pointer; @body)", fn.Get().GetHoverInfo())

variable := fn.Get().Variables["self"]
assert.Equal(t, "self", variable.GetName())
assert.Equal(t, "UserStruct", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetDocumentRange())

variable = fn.Get().Variables["pointer"]
assert.Equal(t, "pointer", variable.GetName())
assert.Equal(t, "int*", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 44, 0, 51), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 39, 0, 51), variable.GetDocumentRange())

variable = fn.Get().Variables["@body"]
assert.Equal(t, "@body", variable.GetName())
assert.Equal(t, "fn void()", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 53, 0, 58), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 53, 0, 58), variable.GetDocumentRange())
})

t.Run("Finds method macro arguments, where @body has parameters", func(t *testing.T) {
source := `macro Object* UserStruct.@method(self, int* pointer; @body(&something, int a, float* b)) {
return 1;
}`
docId := "docId"
doc := document.NewDocument(docId, source)
parser := createParser()
symbols, _ := parser.ParseSymbols(&doc)

fn := symbols.Get("docid").GetChildrenFunctionByName("UserStruct.@method")
assert.True(t, fn.IsSome(), "Method was not found")

assert.Equal(t, "macro Object* UserStruct.@method(UserStruct self, int* pointer; @body(&something, int a, float* b))", fn.Get().GetHoverInfo())

variable := fn.Get().Variables["self"]
assert.Equal(t, "self", variable.GetName())
assert.Equal(t, "UserStruct", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetDocumentRange())

variable = fn.Get().Variables["pointer"]
assert.Equal(t, "pointer", variable.GetName())
assert.Equal(t, "int*", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 44, 0, 51), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 39, 0, 51), variable.GetDocumentRange())

variable = fn.Get().Variables["@body"]
assert.Equal(t, "@body", variable.GetName())
assert.Equal(t, "fn void(&something, int a, float* b)", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 53, 0, 58), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 53, 0, 87), variable.GetDocumentRange())

})

t.Run("Finds method macro arguments, where member reference is a pointer", func(t *testing.T) {
t.Skip("Incomplete until detecting & in self argument")
source := `macro Object* UserStruct.@method(&self, int* pointer; @body) {
return 1;
}`
docId := "docId"
doc := document.NewDocument(docId, source)
parser := createParser()
symbols, _ := parser.ParseSymbols(&doc)

fn := symbols.Get("docid").GetChildrenFunctionByName("UserStruct.@method")
assert.True(t, fn.IsSome(), "Method was not found")

variable := fn.Get().Variables["self"]
assert.Equal(t, "self", variable.GetName())
assert.Equal(t, "UserStruct", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 33, 0, 37), variable.GetDocumentRange())

variable = fn.Get().Variables["pointer"]
assert.Equal(t, "pointer", variable.GetName())
assert.Equal(t, "int*", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 44, 0, 51), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 39, 0, 51), variable.GetDocumentRange())

variable = fn.Get().Variables["@body"]
assert.Equal(t, "@body", variable.GetName())
assert.Equal(t, "fn void()", variable.GetType().String())
assert.Equal(t, idx.NewRange(0, 53, 0, 58), variable.GetIdRange())
assert.Equal(t, idx.NewRange(0, 53, 0, 58), variable.GetDocumentRange())

})
}

func TestExtractSymbols_flags_types_as_pending_to_be_resolved(t *testing.T) {
t.Run("resolves basic type declaration should not flag type as pending to be resolved", func(t *testing.T) {
source := `int value = 1;`
Expand Down
Loading

0 comments on commit e358da4

Please sign in to comment.