Skip to content

Commit

Permalink
fix: only show one code lens and quick fix code action in OSS files […
Browse files Browse the repository at this point in the history
…IDE-484][IDE-485] (#582)

Co-authored-by: bastiandoetsch <[email protected]>
  • Loading branch information
bastiandoetsch and bastiandoetsch authored Jul 16, 2024
1 parent 8f015d9 commit 5daa97a
Show file tree
Hide file tree
Showing 25 changed files with 714 additions and 252 deletions.
78 changes: 69 additions & 9 deletions application/codeaction/codeaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,82 @@ func (c *CodeActionsService) GetCodeActions(params types.CodeActionParams) []typ
issues := c.IssuesProvider.IssuesForRange(path, r)
logMsg := fmt.Sprint("Found ", len(issues), " issues for path ", path, " and range ", r)
c.logger.Info().Msg(logMsg)
actions := converter.ToCodeActions(issues)

quickFixGroupables := c.getQuickFixGroupablesAndCache(issues)

var updatedIssues []snyk.Issue
if len(quickFixGroupables) != 0 {
updatedIssues = c.updateIssuesWithQuickFix(quickFixGroupables, issues)
} else {
updatedIssues = issues
}

actions := converter.ToCodeActions(updatedIssues)
c.logger.Info().Msg(fmt.Sprint("Returning ", len(actions), " code actions"))
return actions
}

func (c *CodeActionsService) updateIssuesWithQuickFix(quickFixGroupables []types.Groupable, issues []snyk.Issue) []snyk.Issue {
// we only allow one quickfix, so it needs to be grouped
quickFix := c.getQuickFixAction(quickFixGroupables)

var updatedIssues []snyk.Issue
for _, issue := range issues {
groupedActions := []snyk.CodeAction{}
if quickFix != nil {
groupedActions = append(groupedActions, *quickFix)
}

for _, action := range issue.CodeActions {
if action.Uuid != nil {
cached := cachedAction{
issue: issue,
action: action,
}
c.actionsCache[*action.Uuid] = cached
if action.GroupingType == types.Quickfix {
continue
}
groupedActions = append(groupedActions, action)
}

issue.CodeActions = groupedActions
updatedIssues = append(updatedIssues, issue)
}

c.logger.Info().Msg(fmt.Sprint("Returning ", len(actions), " code actions"))
return actions
return updatedIssues
}

func (c *CodeActionsService) getQuickFixAction(quickFixGroupables []types.Groupable) *snyk.CodeAction {
// right now we can always group by max semver version, as
// code only has one quickfix available, and iac none at all
var quickFix *snyk.CodeAction
qf, ok := types.MaxSemver()(quickFixGroupables).(snyk.CodeAction)
if !ok {
c.logger.Warn().Msg("grouping quick fix actions failed")
quickFix = nil
} else {
quickFix = &qf
}
c.logger.Debug().Msgf("chose quickfix %s", quickFix.Title)
return quickFix
}

func (c *CodeActionsService) getQuickFixGroupablesAndCache(issues []snyk.Issue) []types.Groupable {
quickFixGroupables := []types.Groupable{}
for _, issue := range issues {
for _, action := range issue.CodeActions {
if action.GroupingType == types.Quickfix {
quickFixGroupables = append(quickFixGroupables, action)
}
c.cacheCodeAction(action, issue)
}
}
return quickFixGroupables
}

func (c *CodeActionsService) cacheCodeAction(action snyk.CodeAction, issue snyk.Issue) {
if action.Uuid != nil {
cached := cachedAction{
issue: issue,
action: action,
}
c.actionsCache[*action.Uuid] = cached
}
}

func (c *CodeActionsService) ResolveCodeAction(action types.CodeAction, server types.Server) (types.CodeAction, error) {
Expand Down
20 changes: 11 additions & 9 deletions application/codeaction/codeaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func Test_GetCodeActions_ReturnsCorrectActions(t *testing.T) {
},
},
}
service, codeActionsParam, _ := setupWithSingleIssue(expectedIssue)
service, codeActionsParam, _ := setupWithSingleIssue(t, expectedIssue)

// Act
actions := service.GetCodeActions(codeActionsParam)
Expand All @@ -104,7 +104,7 @@ func Test_GetCodeActions_FileIsDirty_ReturnsEmptyResults(t *testing.T) {
},
},
}
service, codeActionsParam, w := setupWithSingleIssue(fakeIssue)
service, codeActionsParam, w := setupWithSingleIssue(t, fakeIssue)
w.SetFileAsChanged(codeActionsParam.TextDocument.URI) // File is dirty until it is saved

// Act
Expand Down Expand Up @@ -169,7 +169,7 @@ func Test_ResolveCodeAction_ReturnsCorrectEdit(t *testing.T) {
},
},
}
service, codeActionsParam, _ := setupWithSingleIssue(expectedIssue)
service, codeActionsParam, _ := setupWithSingleIssue(t, expectedIssue)

// Act
actions := service.GetCodeActions(codeActionsParam)
Expand All @@ -187,7 +187,7 @@ func Test_ResolveCodeAction_ReturnsCorrectEdit(t *testing.T) {
func Test_ResolveCodeAction_KeyDoesNotExist_ReturnError(t *testing.T) {
testutil.UnitTest(t)
// Arrange
service := setupService()
service := setupService(t)

id := types.CodeActionData(uuid.New())
ca := types.CodeAction{
Expand All @@ -208,7 +208,7 @@ func Test_ResolveCodeAction_KeyDoesNotExist_ReturnError(t *testing.T) {
func Test_ResolveCodeAction_UnknownCommandIsReported(t *testing.T) {
testutil.UnitTest(t)
// Arrange
service := setupService()
service := setupService(t)
command.SetService(command.NewService(
nil,
nil,
Expand Down Expand Up @@ -243,7 +243,7 @@ func Test_ResolveCodeAction_UnknownCommandIsReported(t *testing.T) {
func Test_ResolveCodeAction_CommandIsExecuted(t *testing.T) {
testutil.UnitTest(t)
// Arrange
service := setupService()
service := setupService(t)

id := types.CodeActionData(uuid.New())
command.SetService(types.NewCommandServiceMock())
Expand All @@ -269,7 +269,7 @@ func Test_ResolveCodeAction_CommandIsExecuted(t *testing.T) {

func Test_ResolveCodeAction_KeyIsNull_ReturnsError(t *testing.T) {
testutil.UnitTest(t)
service := setupService()
service := setupService(t)

ca := types.CodeAction{
Title: "Made up CA",
Expand All @@ -283,7 +283,8 @@ func Test_ResolveCodeAction_KeyIsNull_ReturnsError(t *testing.T) {
assert.True(t, codeaction.IsMissingKeyError(err))
}

func setupService() *codeaction.CodeActionsService {
func setupService(t *testing.T) *codeaction.CodeActionsService {
t.Helper()
providerMock := new(mockIssuesProvider)
providerMock.On("IssuesForRange", mock.Anything, mock.Anything).Return([]snyk.Issue{})
fakeClient := &code.FakeSnykCodeClient{C: config.CurrentConfig()}
Expand All @@ -292,7 +293,8 @@ func setupService() *codeaction.CodeActionsService {
return service
}

func setupWithSingleIssue(issue snyk.Issue) (*codeaction.CodeActionsService, types.CodeActionParams, *watcher.FileWatcher) {
func setupWithSingleIssue(t *testing.T, issue snyk.Issue) (*codeaction.CodeActionsService, types.CodeActionParams, *watcher.FileWatcher) {
t.Helper()
r := exampleRange
uriPath := documentUriExample
path := uri.PathFromUri(uriPath)
Expand Down
10 changes: 8 additions & 2 deletions application/config/automatic_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package config

import (
"fmt"
"io/fs"
"os"
"os/exec"
Expand Down Expand Up @@ -61,7 +62,12 @@ func (c *Config) normalizePath(foundPath string) (string, bool) {
return path, false
}

func (c *Config) determineMavenHome() {
func (c *Config) mavenDefaults() {
// explicitly and always use headless mode
mavenOptsVarName := "MAVEN_OPTS"
mavenOpts := fmt.Sprintf("%s %s", os.Getenv(mavenOptsVarName), "-Djava.awt.headless=true")
_ = os.Setenv(mavenOptsVarName, mavenOpts)

mavenHome := os.Getenv("MAVEN_HOME")
if mavenHome != "" {
c.updatePath(mavenHome + string(os.PathSeparator) + "bin")
Expand All @@ -76,7 +82,7 @@ func (c *Config) determineMavenHome() {
return
}
c.updatePath(filepath.Dir(path))
c.Logger().Info().Str("method", "determineMavenHome").Msgf("detected maven binary at %s", path)
c.Logger().Info().Str("method", "mavenDefaults").Msgf("detected maven binary at %s", path)
}

func getJavaBinaryName() string {
Expand Down
10 changes: 6 additions & 4 deletions application/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,13 @@ func New() *Config {

func initWorkFlowEngine(c *Config) {
conf := configuration.NewInMemory()
conf.Set(cli_constants.EXECUTION_MODE_KEY, cli_constants.EXECUTION_MODE_VALUE_STANDALONE)
enableOAuth := c.authenticationMethod == types.OAuthAuthentication
conf.Set(configuration.FF_OAUTH_AUTH_FLOW_ENABLED, enableOAuth)

c.engine = app.CreateAppEngineWithOptions(app.WithConfiguration(conf), app.WithZeroLogger(c.logger))
c.storage = storage.NewStorage()
conf.SetStorage(c.storage)
conf.Set(configuration.FF_OAUTH_AUTH_FLOW_ENABLED, true)
conf.Set(cli_constants.EXECUTION_MODE_KEY, cli_constants.EXECUTION_MODE_VALUE_STANDALONE)

err := localworkflows.InitWhoAmIWorkflow(c.engine)
if err != nil {
Expand Down Expand Up @@ -283,7 +285,7 @@ func getNewScrubbingLogger(c *Config) *zerolog.Logger {
func (c *Config) AddBinaryLocationsToPath(searchDirectories []string) {
c.defaultDirs = searchDirectories
c.determineJavaHome()
c.determineMavenHome()
c.mavenDefaults()
}

func (c *Config) determineDeviceId() string {
Expand Down Expand Up @@ -738,7 +740,7 @@ func (c *Config) addDefaults() {
c.updatePath(xdg.Home + "/bin")
}
c.determineJavaHome()
c.determineMavenHome()
c.mavenDefaults()
}

func (c *Config) SetIntegrationName(integrationName string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
package codeaction
/*
* © 2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package server

import (
"context"
"sync"
"time"

"github.com/snyk/snyk-ls/application/codeaction"
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/application/di"
"github.com/snyk/snyk-ls/internal/types"
)

type TextDocumentCodeActionHandler func(context.Context, types.CodeActionParams) ([]types.CodeAction, error)
type ResolveHandler func(context.Context, types.CodeAction) (*types.CodeAction, error)

// ResolveCodeActionHandler returns a jrpc2.Handler that can be used to handle the "codeAction/resolve" LSP method
func ResolveCodeActionHandler(c *config.Config, service *CodeActionsService, server types.Server) ResolveHandler {
func ResolveCodeActionHandler(c *config.Config, service *codeaction.CodeActionsService, server types.Server) ResolveHandler {
logger := c.Logger().With().Str("method", "ResolveCodeActionHandler").Logger()
return func(ctx context.Context, params types.CodeAction) (*types.CodeAction, error) {
logger = logger.With().Interface("request", params).Logger()
logger.Info().Msg("RECEIVING")

action, err := service.ResolveCodeAction(params, server)
if err != nil {
if IsMissingKeyError(err) { // If the key is missing, it means that the code action is not a deferred code action
if codeaction.IsMissingKeyError(err) { // If the key is missing, it means that the code action is not a deferred code action
logger.Debug().Msg("Skipping code action - missing key")
return nil, nil
}
Expand All @@ -34,7 +52,7 @@ func ResolveCodeActionHandler(c *config.Config, service *CodeActionsService, ser
}

// GetCodeActionHandler returns a jrpc2.Handler that can be used to handle the "textDocument/codeAction" LSP method
func GetCodeActionHandler(c *config.Config, service *CodeActionsService) TextDocumentCodeActionHandler {
func GetCodeActionHandler(c *config.Config) TextDocumentCodeActionHandler {
const debounceDuration = 50 * time.Millisecond

// We share a mutex between all the handler calls to prevent concurrent runs.
Expand Down Expand Up @@ -74,7 +92,7 @@ func GetCodeActionHandler(c *config.Config, service *CodeActionsService) TextDoc
}

// Fetch & return the code actions
codeActions := service.GetCodeActions(params)
codeActions := di.CodeActionService().GetCodeActions(params)
logger.Info().Any("response", codeActions).Msg("SENDING")
return codeActions, nil
}
Expand Down
28 changes: 10 additions & 18 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"github.com/shirou/gopsutil/process"
sglsp "github.com/sourcegraph/go-lsp"

"github.com/snyk/snyk-ls/application/codeaction"
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/application/di"
"github.com/snyk/snyk-ls/domain/ide/codelens"
Expand Down Expand Up @@ -142,32 +141,25 @@ func codeLensHandler() jrpc2.Handler {
return handler.New(func(ctx context.Context, params sglsp.CodeLensParams) ([]sglsp.CodeLens, error) {
c := config.CurrentConfig()
c.Logger().Info().Str("method", "CodeLensHandler").Msg("RECEIVING")
defer c.Logger().Info().Str("method", "CodeLensHandler").Msg("SENDING")

lenses := codelens.GetFor(uri.PathFromUri(params.TextDocument.URI))

// Do not return Snyk Code Fix codelens when a doc is dirty
isDirtyFile := di.FileWatcher().IsDirty(params.TextDocument.URI)

defer c.Logger().Info().Str("method", "CodeLensHandler").
Bool("isDirtyFile", isDirtyFile).
Int("lensCount", len(lenses)).
Msg("SENDING")

if !isDirtyFile {
return lenses, nil
}

return filterCodeFixCodelens(lenses), nil
// if dirty, lenses don't make sense
return nil, nil
})
}

func filterCodeFixCodelens(lenses []sglsp.CodeLens) []sglsp.CodeLens {
var filteredLenses []sglsp.CodeLens
for _, lense := range lenses {
if lense.Command.Command == types.CodeFixCommand {
continue
}

filteredLenses = append(filteredLenses, lense)
}
return filteredLenses
}

func workspaceDidChangeWorkspaceFoldersHandler(srv *jrpc2.Server) jrpc2.Handler {
return handler.New(func(ctx context.Context, params types.DidChangeWorkspaceFoldersParams) (any, error) {
// The context provided by the JSON-RPC server is canceled once a new message is being processed,
Expand Down Expand Up @@ -583,12 +575,12 @@ func windowWorkDoneProgressCancelHandler() jrpc2.Handler {

func codeActionResolveHandler(server types.Server) handler.Func {
c := config.CurrentConfig()
return handler.New(codeaction.ResolveCodeActionHandler(c, di.CodeActionService(), server))
return handler.New(ResolveCodeActionHandler(c, di.CodeActionService(), server))
}

func textDocumentCodeActionHandler() handler.Func {
c := config.CurrentConfig()
return handler.New(codeaction.GetCodeActionHandler(c, di.CodeActionService()))
return handler.New(GetCodeActionHandler(c))
}

func noOpHandler() jrpc2.Handler {
Expand Down
Loading

0 comments on commit 5daa97a

Please sign in to comment.