Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ add fallbacks for when workspace/symbol isn't giving us what we need #304

Merged
merged 4 commits into from
Sep 14, 2023
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
18 changes: 18 additions & 0 deletions external-providers/generic-external-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ func main() {
s := provider.NewServer(client, *port, log)
ctx := context.TODO()
s.Start(ctx)
// serviceClient, err := client.Init(ctx, log, provider.InitConfig{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this, but being able to run the provider directly has been really nice for debugging. Maybe should add a debug subcommand or something for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it a hidden subcommand in the main binary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it can work in the main binary, would have to be in the provider binaries since the communication between the two is over RPC. Here you can delve debug / see the print statements.

Side note, we really should be capturing the stdout from these providers and presenting them to the users, I don't think we do currently (right?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in a real way, There is the plumbing to do it though

We have one set of questions with that, though,

  1. Do we print it out as it happens, or do we bunch them together once a given evaluation is finished
  2. Are there log levels that need to be returned, is this a verbosity thing where we only print on high levels?

Let me know if you want me to take a stab at it. @pranavgaikwad I am out of the loop on what is important now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should print them as they happen, I wouldn't necessarily bother with verbosity levels as a first-class thing since a provider can always implement verbosity levels through providerSpecificConfig. So we'd just want to log with an identifier that lets you know which provider name is logging and do it in real time so that hangs/etc can be debugged

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you wanted to take a stab at it it would be a huge boon to developing providers. such a PITA right now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shawn-hurley @fabianvf let me know if this sums up the work #312

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do then!

// Location: "/home/fabian/projects/github.com/konveyor/analyzer-lsp/examples/golang",
// AnalysisMode: "full",
// ProviderSpecificConfig: map[string]interface{}{
// "name": "go",
// "lspServerPath": "gopls",
// "lspArgs": []interface{}{"-vv", "-logfile", "go-debug.log", "-rpc.trace"},
// },
// })
// if err != nil {
// panic(err)
// }

// response, err := serviceClient.Evaluate("referenced", []byte(`{"referenced":{pattern: ".*v1beta1.CustomResourceDefinition"}}`))
// if err != nil {
// panic(err)
// }
// fmt.Println(response)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package generic

import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"

"github.com/go-logr/logr"
"github.com/konveyor/analyzer-lsp/jsonrpc2"
Expand Down Expand Up @@ -45,7 +49,7 @@ func (p *genericServiceClient) Evaluate(cap string, conditionInfo []byte) (provi

incidents := []provider.IncidentContext{}
for _, s := range symbols {
references := p.GetAllReferences(s)
references := p.GetAllReferences(s.Location)
for _, ref := range references {
// Look for things that are in the location loaded, //Note may need to filter out vendor at some point
if strings.Contains(ref.URI, p.config.Location) {
Expand Down Expand Up @@ -74,30 +78,133 @@ func (p *genericServiceClient) Evaluate(cap string, conditionInfo []byte) (provi
}, nil
}

func (p *genericServiceClient) GetAllSymbols(query string) []protocol.WorkspaceSymbol {
func processFile(path string, regex *regexp.Regexp, positionsChan chan<- protocol.TextDocumentPositionParams, wg *sync.WaitGroup) {
defer wg.Done()

content, err := os.ReadFile(path)
if err != nil {
return
}

if regex.Match(content) {
scanner := bufio.NewScanner(strings.NewReader(string(content)))
lineNumber := 0
for scanner.Scan() {
matchLocations := regex.FindAllStringIndex(scanner.Text(), -1)
for _, loc := range matchLocations {
absPath, err := filepath.Abs(path)
if err != nil {
return
}
positionsChan <- protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: fmt.Sprintf("file://%s", absPath),
},
Position: protocol.Position{
Line: float64(lineNumber),
Character: float64(loc[1]),
},
}
}
lineNumber++
}
}
}

func parallelWalk(location string, regex *regexp.Regexp) ([]protocol.TextDocumentPositionParams, error) {
var positions []protocol.TextDocumentPositionParams
positionsChan := make(chan protocol.TextDocumentPositionParams)
wg := &sync.WaitGroup{}

go func() {
err := filepath.Walk(location, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}

if f.Mode().IsRegular() {
wg.Add(1)
go processFile(path, regex, positionsChan, wg)
}

return nil
})

if err != nil {
return
}

wg.Wait()
close(positionsChan)
}()

for pos := range positionsChan {
positions = append(positions, pos)
}

return positions, nil
}

func (p *genericServiceClient) GetAllSymbols(query string) []protocol.WorkspaceSymbol {
wsp := &protocol.WorkspaceSymbolParams{
Query: query,
}

var refs []protocol.WorkspaceSymbol
var symbols []protocol.WorkspaceSymbol
fmt.Printf("\nrpc call\n")
err := p.rpc.Call(context.TODO(), "workspace/symbol", wsp, &refs)
err := p.rpc.Call(context.TODO(), "workspace/symbol", wsp, &symbols)
fmt.Printf("\nrpc called\n")
if err != nil {
fmt.Printf("\n\nerror: %v\n", err)
}

return refs
regex, err := regexp.Compile(query)
if err != nil {
// Not a valid regex, can't do anything more
return symbols
}
if len(symbols) == 0 {
// Run empty string query and manually search using the query as a regex
var allSymbols []protocol.WorkspaceSymbol
err = p.rpc.Call(context.TODO(), "workspace/symbol", &protocol.WorkspaceSymbolParams{Query: ""}, &allSymbols)
if err != nil {
fmt.Printf("\n\nerror: %v\n", err)
}
for _, s := range allSymbols {
if regex.MatchString(s.Name) {
symbols = append(symbols, s)
}
}
}
if len(symbols) == 0 {
var positions []protocol.TextDocumentPositionParams
// Fallback to manually searching for an occurrence and performing a GotoDefinition call
positions, err := parallelWalk(p.config.Location, regex)
if err != nil {
fmt.Printf("\n\nerror: %v\n", err)
return nil
}
for _, position := range positions {
fmt.Println(position)
res := []protocol.Location{}
err := p.rpc.Call(p.ctx, "textDocument/definition", position, &res)
if err != nil {
fmt.Printf("Error rpc: %v", err)
}
for _, r := range res {
symbols = append(symbols, protocol.WorkspaceSymbol{Location: r})
}
}
}
return symbols
}

func (p *genericServiceClient) GetAllReferences(symbol protocol.WorkspaceSymbol) []protocol.Location {
func (p *genericServiceClient) GetAllReferences(location protocol.Location) []protocol.Location {
params := &protocol.ReferenceParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: symbol.Location.URI,
URI: location.URI,
},
Position: symbol.Location.Range.Start,
Position: location.Range.Start,
},
}

Expand Down
Binary file not shown.
4 changes: 2 additions & 2 deletions rule-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
pattern: "*apiextensions.v1beta1.CustomResourceDefinition*"
location: TYPE
- go.referenced:
pattern: "v1beta1.CustomResourceDefinition"
pattern: ".*v1beta1.CustomResourceDefinition"
- message: 'golang apiextensions/v1/customresourcedefinitions found {{file}}:{{lineNumber}}'
ruleID: go-lang-ref-001
when:
go.referenced:
pattern: "v1beta1.CustomResourceDefinition"
pattern: ".*v1beta1.CustomResourceDefinition"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should make it trigger the fallback

- message: testing nested conditions
ruleID: lang-ref-002
when:
Expand Down