Skip to content

Commit

Permalink
member readable conditional search
Browse files Browse the repository at this point in the history
  • Loading branch information
PgBiel committed Jan 26, 2025
1 parent 85cace6 commit aaa6299
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 18 deletions.
66 changes: 49 additions & 17 deletions server/internal/lsp/search/find_parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import (
"github.com/pherrymason/c3-lsp/pkg/symbols"
)

// If `true`, this indexable is a type, and so one can access its parent type's (itself)
// associated members, as well as methods.
// If `false`, this indexable is a variable or similar, so its parent type is distinct
// from the indexable itself, therefore only methods can be accessed.
func canReadMembersOf(s symbols.Indexable) bool {
switch s.(type) {
case *symbols.Struct, *symbols.Enum, *symbols.Fault:
return true
case *symbols.Def:
// If Def resolves to a type, it can receive its members.
return s.(*symbols.Def).ResolvesToType()
default:
return false
}
}

// Search for a method for 'parentTypeName' given a symbol to search.
//
// Returns updated search parameters to progress the search, as well as
Expand Down Expand Up @@ -60,6 +76,7 @@ func (s *Search) findInParentSymbols(searchParams search_params.SearchParams, pr
return searchResult
}
protection++
membersReadable := canReadMembersOf(elm)

for {
if !isInspectable(elm) {
Expand Down Expand Up @@ -94,18 +111,25 @@ func (s *Search) findInParentSymbols(searchParams search_params.SearchParams, pr

case *symbols.Enum:
_enum := elm.(*symbols.Enum)
enumerators := _enum.GetEnumerators()
searchingSymbol := state.GetNextSymbol()
foundMember := false
for i := 0; i < len(enumerators); i++ {
if enumerators[i].GetName() == searchingSymbol.Text() {
elm = enumerators[i]
symbolsHierarchy = append(symbolsHierarchy, elm)
state.Advance()
foundMember = true
break
searchingSymbol := state.GetNextSymbol()

// 'CoolEnum.VARIANT.VARIANT' is invalid (member not readable on member)
// But 'CoolEnum.VARIANT' is ok,
// as well as 'AliasForEnum.VARIANT'
if membersReadable {
enumerators := _enum.GetEnumerators()
for i := 0; i < len(enumerators); i++ {
if enumerators[i].GetName() == searchingSymbol.Text() {
elm = enumerators[i]
symbolsHierarchy = append(symbolsHierarchy, elm)
state.Advance()
foundMember = true
break
}
}
}

if !foundMember {
// Search in methods
newIterSearch, result := s.findMethod(
Expand All @@ -127,16 +151,19 @@ func (s *Search) findInParentSymbols(searchParams search_params.SearchParams, pr

case *symbols.Fault:
fault := elm.(*symbols.Fault)
constants := fault.GetConstants()
searchingSymbol := state.GetNextSymbol()
foundMember := false
for i := 0; i < len(constants); i++ {
if constants[i].GetName() == searchingSymbol.Text() {
elm = constants[i]
symbolsHierarchy = append(symbolsHierarchy, elm)
state.Advance()
foundMember = true
break

if membersReadable {
constants := fault.GetConstants()
for i := 0; i < len(constants); i++ {
if constants[i].GetName() == searchingSymbol.Text() {
elm = constants[i]
symbolsHierarchy = append(symbolsHierarchy, elm)
state.Advance()
foundMember = true
break
}
}
}

Expand Down Expand Up @@ -164,6 +191,11 @@ func (s *Search) findInParentSymbols(searchParams search_params.SearchParams, pr
members := strukt.GetMembers()
searchingSymbol := state.GetNextSymbol()
foundMember := false

// Members are always readable when the parent type is struct
// TODO: Maybe we should actually check for NOT membersReadable,
// if anonymous substructs are found to not be usable anywhere
// (Can't write methods for them, for example)
for i := 0; i < len(members); i++ {
if members[i].GetName() == searchingSymbol.Text() {
elm = members[i]
Expand Down
63 changes: 63 additions & 0 deletions server/internal/lsp/search/search_closest_declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,38 @@ func TestLanguage_findClosestSymbolDeclaration_enums(t *testing.T) {
assert.Equal(t, "BACKGROUND", symbolOption.Get().GetName())
})

t.Run("Should not find enumerator on enumerator", func(t *testing.T) {
state.registerDoc(
"app.c3",
`enum WindowStatus { OPEN, BACKGROUND, MINIMIZED }
fn void main() {
WindowStatus status;
status = WindowStatus.BACKGROUND.MINIMIZED;
}`,
)
position := buildPosition(4, 38) // Cursor is at `status = WindowStatus.BACKGROUND.M|INIMIZED`

symbolOption := search.FindSymbolDeclarationInWorkspace("app.c3", position, &state.state)

assert.True(t, symbolOption.IsNone(), "Element found")
})

t.Run("Should not find enumerator on enumerator variable", func(t *testing.T) {
state.registerDoc(
"app.c3",
`enum WindowStatus { OPEN, BACKGROUND, MINIMIZED }
fn void main() {
WindowStatus status = WindowStatus.BACKGROUND;
status = status.MINIMIZED;
}`,
)
position := buildPosition(4, 21) // Cursor is at `status = status.M|INIMIZED`

symbolOption := search.FindSymbolDeclarationInWorkspace("app.c3", position, &state.state)

assert.True(t, symbolOption.IsNone(), "Element found")
})

t.Run("Should find local enumerator definition associated value", func(t *testing.T) {
state.registerDoc(
"app.c3",
Expand Down Expand Up @@ -459,6 +491,37 @@ func TestLanguage_findClosestSymbolDeclaration_faults(t *testing.T) {
assert.Equal(t, "UNEXPECTED_ERROR", symbolOption.Get().GetName())
})

t.Run("Should not find fault constant on fault constant", func(t *testing.T) {
state.registerDoc(
"app.c3",
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
fn void main() {
WindowError.SOMETHING_HAPPENED.UNEXPECTED_ERROR;
}`,
)
position := buildPosition(3, 36) // Cursor at `WindowError.SOMETHING_HAPPENED.U|NEXPECTED_ERROR;`

symbolOption := search.FindSymbolDeclarationInWorkspace("app.c3", position, &state.state)

assert.True(t, symbolOption.IsNone(), "Element found")
})

t.Run("Should not find fault constant on fault instance", func(t *testing.T) {
state.registerDoc(
"app.c3",
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
fn void main() {
WindowError error = WindowError.SOMETHING_HAPPENED;
error.UNEXPECTED_ERROR;
}`,
)
position := buildPosition(4, 11) // Cursor at `error.U|NEXPECTED_ERROR;`

symbolOption := search.FindSymbolDeclarationInWorkspace("app.c3", position, &state.state)

assert.True(t, symbolOption.IsNone(), "Element found")
})

t.Run("Should find fault method definition", func(t *testing.T) {
state.registerDoc(
"app.c3",
Expand Down
82 changes: 82 additions & 0 deletions server/internal/lsp/search/search_completion_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,88 @@ func TestBuildCompletionList_faults(t *testing.T) {
})
}
})

t.Run("Should not suggest Fault constant type after explicit constant", func(t *testing.T) {
source := `
fault WindowError { COH, COUGH, COUGHCOUGH}
fault WindowFileError { NOT_FOUND, NO_PERMISSIONS, COULD_NOT_CREATE }
fn void main() {
`
cases := []struct {
name string
input string
expected []protocol.CompletionItem
}{
{
"Do not find constants prefixed with fault constant",
"WindowFileError.NOT_FOUND.",
nil},
{
"Do not find matching constants prefixed with fault constant",
"WindowFileError.NOT_FOUND.NO_PE",
nil},
}

for _, tt := range cases {
t.Run(fmt.Sprintf("Autocomplete contants: #%s", tt.name), func(t *testing.T) {
state := NewTestState()
state.registerDoc("test.c3", source+tt.input+`}`)
position := buildPosition(5, uint(len(tt.input))) // Cursor after `<input>|`

search := NewSearchWithoutLog()
completionList := search.BuildCompletionList(
context.CursorContext{
Position: position,
DocURI: "test.c3",
},
&state.state)

assert.Equal(t, len(tt.expected), len(completionList))
assert.Equal(t, tt.expected, completionList)
})
}
})

t.Run("Should not suggest Fault constant type after instance", func(t *testing.T) {
source := `
fault WindowFileError { NOT_FOUND, NO_PERMISSIONS, COULD_NOT_CREATE }
fn void main() {
WindowFileError inst = NOT_FOUND;
`
cases := []struct {
name string
input string
expected []protocol.CompletionItem
}{
{
"Do not find constants prefixed with fault instance",
"inst.",
nil},
{
"Do not find matching constants prefixed with fault instance",
"inst.NO_PE",
nil},
}

for _, tt := range cases {
t.Run(fmt.Sprintf("Autocomplete contants: #%s", tt.name), func(t *testing.T) {
state := NewTestState()
state.registerDoc("test.c3", source+tt.input+`}`)
position := buildPosition(5, uint(len(tt.input))) // Cursor after `<input>|`

search := NewSearchWithoutLog()
completionList := search.BuildCompletionList(
context.CursorContext{
Position: position,
DocURI: "test.c3",
},
&state.state)

assert.Equal(t, len(tt.expected), len(completionList))
assert.Equal(t, tt.expected, completionList)
})
}
})
}

func TestBuildCompletionList_modules(t *testing.T) {
Expand Down
101 changes: 100 additions & 1 deletion server/internal/lsp/search/search_find_access_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,45 @@ func TestProjectState_findClosestSymbolDeclaration_access_path_enums(t *testing.
assert.Equal(t, "OPEN", symbolOption.Get().GetName())
})

t.Run("Should not find enumerator after explicit enumerator path", func(t *testing.T) {
state.registerDoc(
"enums.c3",
`enum WindowStatus { OPEN, BACKGROUND, MINIMIZED }
WindowStatus stat = WindowStatus.OPEN.BACKGROUND;`,
)
position := buildPosition(2, 42) // Cursor at `WindowStatus stat = WindowStatus.OPEN.B|ACKGROUND;`
doc := state.GetDoc("enums.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
*state.state.GetUnitModulesByDoc(doc.URI),
position,
)

symbolOption := search.findClosestSymbolDeclaration(searchParams, &state.state, debugger)

assert.True(t, symbolOption.IsNone(), "Element was found")
})

t.Run("Should not find enumerator after instance variable", func(t *testing.T) {
state.registerDoc(
"enums.c3",
`enum WindowStatus { OPEN, BACKGROUND, MINIMIZED }
WindowStatus stat = WindowStatus.OPEN;
WindoWStatus bad = stat.BACKGROUND;`,
)
position := buildPosition(3, 28) // Cursor at `WindoWStatus bad = stat.B|ACKGROUND;`
doc := state.GetDoc("enums.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
*state.state.GetUnitModulesByDoc(doc.URI),
position,
)

symbolOption := search.findClosestSymbolDeclaration(searchParams, &state.state, debugger)

assert.True(t, symbolOption.IsNone(), "Element was found")
})

t.Run("Should find enum method", func(t *testing.T) {
state.registerDoc(
"enums.c3",
Expand Down Expand Up @@ -452,7 +491,7 @@ func TestProjectState_findClosestSymbolDeclaration_access_path_faults(t *testing
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
WindowError err = WindowError.UNEXPECTED_ERROR;`,
)
position := buildPosition(2, 34) // Cursor at `WindowStatus stat = WindowStatus.O|PEN;`
position := buildPosition(2, 34) // Cursor at `WindowError err = WindowError.U|NEXPECTED_ERROR;`
doc := state.GetDoc("app.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
Expand All @@ -468,6 +507,66 @@ func TestProjectState_findClosestSymbolDeclaration_access_path_faults(t *testing
assert.Equal(t, "UNEXPECTED_ERROR", symbolOption.Get().GetName())
})

t.Run("Should not find fault constant after explicit instance", func(t *testing.T) {
state.registerDoc(
"app.c3",
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
WindowError err = WindowError.UNEXPECTED_ERROR.SOMETHING_HAPPENED;`,
)
position := buildPosition(2, 51) // Cursor at `WindowError err = WindowError.UNEXPECTED_ERROR.S|OMETHING_HAPPENED;`
doc := state.GetDoc("app.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
*state.state.GetUnitModulesByDoc(doc.URI),
position,
)

symbolOption := search.findClosestSymbolDeclaration(searchParams, &state.state, debugger)

assert.True(t, symbolOption.IsNone(), "Constant was wrongly found on instance")
})

t.Run("Should not find fault constant after instance variable", func(t *testing.T) {
state.registerDoc(
"app.c3",
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
WindowError err = WindowError.UNEXPECTED_ERROR;
WindowError bad = err.SOMETHING_HAPPENED;`,
)
position := buildPosition(3, 26) // Cursor at `WindowError bad = err.S|OMETHING_HAPPENED;`
doc := state.GetDoc("app.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
*state.state.GetUnitModulesByDoc(doc.URI),
position,
)

symbolOption := search.findClosestSymbolDeclaration(searchParams, &state.state, debugger)

assert.True(t, symbolOption.IsNone(), "Constant was wrongly found on instance variable")
})

t.Run("Should not find fault constant after instance variable in struct member", func(t *testing.T) {
state.registerDoc(
"app.c3",
`fault WindowError { UNEXPECTED_ERROR, SOMETHING_HAPPENED }
struct MyStruct { WindowError f; }
MyStruct st = { WindowError.UNEXPECTED_ERROR };
WindowError bad = st.SOMETHING_HAPPENED;`,
)
position := buildPosition(4, 25) // Cursor at `WindowError bad = st.S|OMETHING_HAPPENED;`
doc := state.GetDoc("app.c3")
searchParams := search_params.BuildSearchBySymbolUnderCursor(
&doc,
*state.state.GetUnitModulesByDoc(doc.URI),
position,
)

symbolOption := search.findClosestSymbolDeclaration(searchParams, &state.state, debugger)

assert.True(t, symbolOption.IsNone(), "Constant was wrongly found on instance variable")
})

t.Run("Should find fault method", func(t *testing.T) {
state.registerDoc(
"app.c3",
Expand Down

0 comments on commit aaa6299

Please sign in to comment.