diff --git a/README.md b/README.md index b30be43bd..e5d97d80e 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,11 @@ Right now the language server supports the following actions: "userMessage": "Optional message to the user" // present if 'ok' is false } ``` +- `Clear Cache` Clears either persisted or inMemory Cache or both. + - command: `snyk.clearCache` + - args: + - `folderUri` string, + - `cacheType` `persisted` or `inMemory` ## Installation diff --git a/application/server/server.go b/application/server/server.go index 2fc89334b..298db85fd 100644 --- a/application/server/server.go +++ b/application/server/server.go @@ -296,6 +296,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func { types.CodeSubmitFixFeedback, types.CodeFixDiffsCommand, types.ExecuteCLICommand, + types.ClearCacheCommand, }, }, }, diff --git a/domain/ide/command/clear_cache.go b/domain/ide/command/clear_cache.go new file mode 100644 index 000000000..5dfc12b7c --- /dev/null +++ b/domain/ide/command/clear_cache.go @@ -0,0 +1,97 @@ +/* + * © 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 command + +import ( + "context" + "github.com/rs/zerolog" + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/internal/types" + "github.com/sourcegraph/go-lsp" + "net/url" +) + +type clearCache struct { + command types.CommandData +} + +func (cmd *clearCache) Command() types.CommandData { + return cmd.command +} + +// Execute Deletes persisted, inMemory Cache or both. +// Parameters: folderUri either folder Uri or empty for all folders +// cacheType: either inMemory or persisted or empty for both. +func (cmd *clearCache) Execute(_ context.Context) (any, error) { + logger := config.CurrentConfig().Logger().With().Str("method", "clearCache.Execute").Logger() + args := cmd.command.Arguments + var parsedFolderUri *lsp.DocumentURI + folderURI := args[0].(string) + + if folderURI != "" { + decodedPath, err := url.PathUnescape(folderURI) + if err != nil { + logger.Error().Err(err).Msgf("could not decode folder Uri %s", folderURI) + return nil, err + } + uri := lsp.DocumentURI(decodedPath) + parsedFolderUri = &uri + } + + cacheType := args[1].(string) + + if cacheType == "" { + cmd.purgeInMemoryCache(&logger, parsedFolderUri) + cmd.purgePersistedCache(&logger, parsedFolderUri) + } else if cacheType == "inMemory" { + cmd.purgeInMemoryCache(&logger, parsedFolderUri) + } else if cacheType == "persisted" { + cmd.purgePersistedCache(&logger, parsedFolderUri) + } + + return nil, nil +} + +func (cmd *clearCache) purgeInMemoryCache(logger *zerolog.Logger, folderUri *lsp.DocumentURI) { + ws := workspace.Get() + for _, folder := range ws.Folders() { + if folderUri != nil && *folderUri != folder.Uri() { + continue + } + logger.Info().Msgf("deleting in-memory cache for folder %s", folder.Path()) + folder.Clear() + } +} + +func (cmd *clearCache) purgePersistedCache(logger *zerolog.Logger, folderUri *lsp.DocumentURI) { + var folderList []string + ws := workspace.Get() + scanPersister := ws.ScanPersister() + if scanPersister == nil { + logger.Error().Msgf("could not find scan persister") + return + } + for _, folder := range ws.Folders() { + if folderUri != nil && *folderUri != folder.Uri() { + continue + } + folderList = append(folderList, folder.Path()) + } + logger.Info().Msgf("deleting perrsisted cache for folders %v", folderList) + scanPersister.Clear(folderList, false) +} diff --git a/domain/ide/command/clear_cache_test.go b/domain/ide/command/clear_cache_test.go new file mode 100644 index 000000000..5328205ec --- /dev/null +++ b/domain/ide/command/clear_cache_test.go @@ -0,0 +1,59 @@ +/* + * © 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 command + +import ( + "context" + "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/domain/snyk/persistence" + "github.com/snyk/snyk-ls/domain/snyk/scanner" + "github.com/snyk/snyk-ls/internal/notification" + "github.com/snyk/snyk-ls/internal/observability/performance" + "github.com/snyk/snyk-ls/internal/testutil" + "github.com/snyk/snyk-ls/internal/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_ClearCache_DeleteAll_NoError(t *testing.T) { + c := testutil.UnitTest(t) + + // Arrange + sc := &scanner.TestScanner{} + scanNotifier := scanner.NewMockScanNotifier() + scanPersister := persistence.NewGitPersistenceProvider(c.Logger()) + w := workspace.New(c, performance.NewInstrumentor(), sc, nil, scanNotifier, notification.NewMockNotifier(), scanPersister) + folder := workspace.NewFolder(c, "dummy", "dummy", sc, nil, scanNotifier, notification.NewMockNotifier(), scanPersister) + w.AddFolder(folder) + workspace.Set(w) + + clearCacheCommand := setupClearCacheCommand(t, "", "") + + // Execute the command + _, err := clearCacheCommand.Execute(context.Background()) + + // Assert + assert.NoError(t, err) +} + +func setupClearCacheCommand(t *testing.T, folderUri, cacheType string) clearCache { + t.Helper() + clearCacheCmd := clearCache{ + command: types.CommandData{Arguments: []interface{}{folderUri, cacheType}}, + } + return clearCacheCmd +} diff --git a/domain/ide/command/command_factory.go b/domain/ide/command/command_factory.go index bed239f55..9cb833465 100644 --- a/domain/ide/command/command_factory.go +++ b/domain/ide/command/command_factory.go @@ -18,7 +18,6 @@ package command import ( "fmt" - "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/domain/snyk" "github.com/snyk/snyk-ls/infrastructure/authentication" @@ -90,6 +89,8 @@ func CreateFromCommandData( }, nil case types.ExecuteCLICommand: return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil + case types.ClearCacheCommand: + return &clearCache{command: commandData}, nil } return nil, fmt.Errorf("unknown command %v", commandData) diff --git a/domain/ide/workspace/folder.go b/domain/ide/workspace/folder.go index 012904ded..99e02be19 100644 --- a/domain/ide/workspace/folder.go +++ b/domain/ide/workspace/folder.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/sourcegraph/go-lsp" "strings" "sync" @@ -646,6 +647,8 @@ func (f *Folder) sendHoversForFile(path string, issues []snyk.Issue) { func (f *Folder) Path() string { return f.path } +func (f *Folder) Uri() lsp.DocumentURI { return uri.PathToUri(f.path) } + func (f *Folder) Name() string { return f.name } func (f *Folder) Status() FolderStatus { return f.status } diff --git a/domain/ide/workspace/workspace.go b/domain/ide/workspace/workspace.go index acf5abec4..b5935cb3e 100644 --- a/domain/ide/workspace/workspace.go +++ b/domain/ide/workspace/workspace.go @@ -100,6 +100,10 @@ func Set(w *Workspace) { instance = w } +func (w *Workspace) ScanPersister() persistence.ScanSnapshotPersister { + return w.scanPersister +} + func (w *Workspace) RemoveFolder(folderPath string) { w.mutex.Lock() defer w.mutex.Unlock() diff --git a/domain/snyk/persistence/git_persistence_provider.go b/domain/snyk/persistence/git_persistence_provider.go index c21af2f79..4cfe79308 100644 --- a/domain/snyk/persistence/git_persistence_provider.go +++ b/domain/snyk/persistence/git_persistence_provider.go @@ -51,7 +51,7 @@ type hashedFolderPath string type ScanSnapshotPersister interface { Init(folderPath []string) error - Clear(folderPath []string, deleteIfExpired bool) + Clear(folderPath []string, deleteOnlyExpired bool) Add(folderPath, commitHash string, issueList []snyk.Issue, p product.Product) error GetPersistedIssueList(folderPath string, p product.Product) ([]snyk.Issue, error) Exists(folderPath, commitHash string, p product.Product) bool diff --git a/internal/types/command.go b/internal/types/command.go index 74b173cef..1643fb9ff 100644 --- a/internal/types/command.go +++ b/internal/types/command.go @@ -39,6 +39,7 @@ const ( GetActiveUserCommand = "snyk.getActiveUser" ReportAnalyticsCommand = "snyk.reportAnalytics" ExecuteCLICommand = "snyk.executeCLI" + ClearCacheCommand = "snyk.clearCache" // Snyk Code specific commands CodeFixCommand = "snyk.code.fix"