diff --git a/provider/internal/java/dependency.go b/provider/internal/java/dependency.go index d387bdcb..7bf1c5dc 100644 --- a/provider/internal/java/dependency.go +++ b/provider/internal/java/dependency.go @@ -12,8 +12,10 @@ import ( "path/filepath" "reflect" "regexp" + "strconv" "strings" + "github.com/konveyor/analyzer-lsp/engine" "github.com/konveyor/analyzer-lsp/engine/labels" "github.com/konveyor/analyzer-lsp/output/v1/konveyor" "github.com/konveyor/analyzer-lsp/provider" @@ -28,6 +30,15 @@ const ( providerSpecificConfigExcludePackagesKey = "excludePackages" ) +// keys used in dep.Extras for extra information about a dep +const ( + artifactIdKey = "artifactId" + groupIdKey = "groupId" + pomPathKey = "pomPath" +) + +var _ provider.DependencyLocationResolver = &javaServiceClient{} + // TODO implement this for real func (p *javaServiceClient) findPom() string { var depPath string @@ -517,3 +528,49 @@ func loadDepLabelItems(r io.Reader, depToLabels map[string]*depLabelItem, label } return nil } + +// getLineNumberForDep given a dep and pom file, attempts to find line number +func (j *javaServiceClient) GetLocation(ctx context.Context, dep konveyor.Dep) (engine.Location, error) { + if dep.Extras == nil { + return engine.Location{}, fmt.Errorf("unable to get location for dep %s", dep.Name) + } + extrasKeys := []string{artifactIdKey, groupIdKey, pomPathKey} + for _, key := range extrasKeys { + if _, ok := dep.Extras[key]; !ok { + return engine.Location{}, fmt.Errorf("unable to get location for dep %s, missing extras", dep.Name) + } + } + groupId := dep.Extras[groupIdKey] + artifactId := dep.Extras[artifactIdKey] + path := dep.Extras[pomPathKey] + cmd := exec.CommandContext(ctx, "pcre2grep", []string{ + "-M", + "-n", + "--line-offsets", + fmt.Sprintf("%s(\\n|.)*?%s.*", groupId, artifactId), + }...) + output, err := cmd.CombinedOutput() + if err != nil { + return engine.Location{}, fmt.Errorf("failed to run pcre2grep to get line number (%w)", err) + } + outputLines := strings.Split(string(output), "\n") + if len(outputLines) < 1 { + return engine.Location{}, fmt.Errorf("no match found for %s.%s in %s", groupId, artifactId, path) + } + parts := strings.Split(outputLines[0], ":") + if len(parts) < 1 { + return engine.Location{}, fmt.Errorf("unparseable pcre2grep output - %s", outputLines[0]) + } + lineNumber, err := strconv.Atoi(parts[0]) + if err != nil { + return engine.Location{}, fmt.Errorf("unparseable pcre2grep output - %s", outputLines[0]) + } + return engine.Location{ + StartPosition: engine.Position{ + Line: lineNumber, + }, + EndPosition: engine.Position{ + Line: lineNumber, + }, + }, nil +} diff --git a/provider/provider.go b/provider/provider.go index 1e2f5b83..d0b18971 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -6,6 +6,7 @@ import ( "os" "regexp" "strings" + "time" "github.com/cbroglie/mustache" "github.com/getkin/kin-openapi/openapi3" @@ -336,6 +337,10 @@ type ServiceClient interface { GetDependenciesDAG(ctx context.Context) (map[uri.URI][]DepDAGItem, error) } +type DependencyLocationResolver interface { + GetLocation(ctx context.Context, dep konveyor.Dep) (engine.Location, error) +} + type Dep = konveyor.Dep type DepDAGItem = konveyor.DepDAGItem type Startable interface { @@ -561,17 +566,34 @@ func (dc DependencyCondition) Evaluate(ctx context.Context, log logr.Logger, con return resp, nil } + var depLocationResolver DependencyLocationResolver + if val, ok := dc.Client.(DependencyLocationResolver); ok { + depLocationResolver = val + } + for _, matchedDep := range matchedDeps { if matchedDep.dep.Version == "" || (dc.Lowerbound == "" && dc.Upperbound == "") { - resp.Matched = true - resp.Incidents = append(resp.Incidents, engine.IncidentContext{ + incident := engine.IncidentContext{ FileURI: matchedDep.uri, Variables: map[string]interface{}{ "name": matchedDep.dep.Name, "version": matchedDep.dep.Version, "type": matchedDep.dep.Type, }, - }) + } + if depLocationResolver != nil { + // this is a best-effort step and we don't want to block if resolver misbehaves + timeoutContext, cancelFunc := context.WithTimeout(ctx, time.Second*3) + location, err := depLocationResolver.GetLocation(timeoutContext, *matchedDep.dep) + if err == nil { + incident.LineNumber = &location.StartPosition.Line + } else { + log.V(7).Error(err, "failed to get location for dependency", "dep", matchedDep.dep.Name) + } + cancelFunc() + } + resp.Matched = true + resp.Incidents = append(resp.Incidents, incident) // For now, lets leave this TODO to figure out what we should be setting in the context resp.TemplateContext = map[string]interface{}{ "name": matchedDep.dep.Name,