Skip to content
Draft
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
90 changes: 75 additions & 15 deletions application/codeaction/codeaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package codeaction

import (
"context"
"errors"
"fmt"
"time"
Expand All @@ -30,8 +31,10 @@ import (
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/domain/ide/converter"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/domain/snyk/remediation"
"github.com/snyk/snyk-ls/infrastructure/featureflag"
noti "github.com/snyk/snyk-ls/internal/notification"
"github.com/snyk/snyk-ls/internal/product"
"github.com/snyk/snyk-ls/internal/types"
"github.com/snyk/snyk-ls/internal/uri"
)
Expand All @@ -47,29 +50,31 @@ type CodeActionsService struct {

// actionsCache holds all the issues that were returns by the GetCodeActions method.
// This is used to resolve the code actions later on in ResolveCodeAction.
actionsCache map[uuid.UUID]cachedAction
engine workflow.Engine
logger zerolog.Logger
fileWatcher dirtyFilesWatcher
notifier noti.Notifier
configResolver types.ConfigResolverInterface
actionsCache map[uuid.UUID]cachedAction
engine workflow.Engine
logger zerolog.Logger
fileWatcher dirtyFilesWatcher
notifier noti.Notifier
configResolver types.ConfigResolverInterface
remediationProvider remediation.RemediationProvider
}

type cachedAction struct {
issue types.Issue
action types.CodeAction
}

func NewService(engine workflow.Engine, provider snyk.IssueProvider, fileWatcher dirtyFilesWatcher, notifier noti.Notifier, featureFlagService featureflag.Service, configResolver types.ConfigResolverInterface) *CodeActionsService {
func NewService(engine workflow.Engine, provider snyk.IssueProvider, fileWatcher dirtyFilesWatcher, notifier noti.Notifier, featureFlagService featureflag.Service, configResolver types.ConfigResolverInterface, remediationProvider remediation.RemediationProvider) *CodeActionsService {
return &CodeActionsService{
IssuesProvider: provider,
featureFlagService: featureFlagService,
actionsCache: make(map[uuid.UUID]cachedAction),
engine: engine,
logger: engine.GetLogger().With().Str("service", "CodeActionsService").Logger(),
fileWatcher: fileWatcher,
notifier: notifier,
configResolver: configResolver,
IssuesProvider: provider,
featureFlagService: featureFlagService,
actionsCache: make(map[uuid.UUID]cachedAction),
engine: engine,
logger: engine.GetLogger().With().Str("service", "CodeActionsService").Logger(),
fileWatcher: fileWatcher,
notifier: notifier,
configResolver: configResolver,
remediationProvider: remediationProvider,
}
}

Expand Down Expand Up @@ -122,11 +127,66 @@ func (c *CodeActionsService) GetCodeActions(params types.CodeActionParams) []typ
updatedIssues = filteredIssues
}

remediationActions := c.remediationCodeActions(updatedIssues, path, folder.Path(), r)
actions := converter.ToCodeActions(updatedIssues)
actions = append(actions, remediationActions...)
c.logger.Debug().Msg(fmt.Sprint("Returning ", len(actions), " code actions"))
return actions
}

func (c *CodeActionsService) remediationCodeActions(issues []types.Issue, path types.FilePath, folderPath types.FilePath, r types.Range) []types.LSPCodeAction {
if c.remediationProvider == nil {
return nil
}
var actions []types.LSPCodeAction
for i := range issues {
issue := issues[i]
findingId := issue.GetFindingId()
if findingId == "" {
continue
}
issueProduct := issue.GetProduct()
if issueProduct == product.ProductSecrets || issueProduct == product.ProductUnknown {
continue
}
additionalData := issue.GetAdditionalData()
if issueProduct != product.ProductInfrastructureAsCode && (additionalData == nil || !additionalData.IsFixable()) {
continue
}
// Capture loop variables for the closure.
issueFindingId := findingId
issueRange := r
provider := c.remediationProvider
deferredEdit := func() *types.WorkspaceEdit {
edit, err := provider.Remediate(context.Background(), remediation.RemediationRequest{
FindingId: issueFindingId,
FilePath: path,
ContentRoot: folderPath,
Range: issueRange,
Product: issueProduct,
})
if err != nil {
c.logger.Error().Err(err).Str("findingId", issueFindingId).Msg("remediation provider returned error")
}
return edit
}
action, err := snyk.NewDeferredCodeAction(
"Fix with Snyk Remediation Agent",
&deferredEdit,
nil,
"",
nil,
)
if err == nil {
action.Kind = types.RemediationAgentQuickFix
lspAction := converter.ToCodeAction(issue, &action)
c.cacheCodeAction(&action, issue)
actions = append(actions, lspAction)
}
}
return actions
}

func (c *CodeActionsService) UpdateIssuesWithQuickFix(quickFixGroupables []types.Groupable, issues []types.Issue) []types.Issue {
// we only allow one quickfix, so it needs to be grouped
quickFix := c.getQuickFixAction(quickFixGroupables)
Expand Down
6 changes: 3 additions & 3 deletions application/codeaction/codeaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func Test_GetCodeActions_NoIssues_ReturnsNil(t *testing.T) {
var issues []types.Issue
providerMock := mock_snyk.NewMockIssueProvider(ctrl)
providerMock.EXPECT().IssuesForRange(gomock.Any(), gomock.Any()).Return(issues)
service := codeaction.NewService(engine, providerMock, watcher.NewFileWatcher(), notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()))
service := codeaction.NewService(engine, providerMock, watcher.NewFileWatcher(), notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()), nil)
codeActionsParam := types.CodeActionParams{
TextDocument: sglsp.TextDocumentIdentifier{
URI: documentUriExample,
Expand Down Expand Up @@ -281,7 +281,7 @@ func setupService(t *testing.T, engine workflow.Engine) *codeaction.CodeActionsS

providerMock := mock_snyk.NewMockIssueProvider(gomock.NewController(t))
providerMock.EXPECT().IssuesForRange(gomock.Any(), gomock.Any()).Return([]types.Issue{}).AnyTimes()
service := codeaction.NewService(engine, providerMock, watcher.NewFileWatcher(), notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()))
service := codeaction.NewService(engine, providerMock, watcher.NewFileWatcher(), notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()), nil)
return service
}

Expand All @@ -299,7 +299,7 @@ func setupWithSingleIssue(t *testing.T, engine workflow.Engine, issue types.Issu
issues := []types.Issue{issue}
providerMock.EXPECT().IssuesForRange(path, converter.FromRange(r)).Return(issues).AnyTimes()
fileWatcher := watcher.NewFileWatcher()
service := codeaction.NewService(engine, providerMock, fileWatcher, notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()))
service := codeaction.NewService(engine, providerMock, fileWatcher, notification.NewMockNotifier(), featureflag.NewFakeService(), types.NewConfigResolver(engine.GetLogger()), nil)

codeActionsParam := types.CodeActionParams{
TextDocument: sglsp.TextDocumentIdentifier{
Expand Down
Loading
Loading