diff --git a/README.md b/README.md index a2ab97d1b..d744d60c1 100644 --- a/README.md +++ b/README.md @@ -492,13 +492,13 @@ within `initializationOptions?: LSPAny;` we support the following settings: "integrationVersion": "1.0.0", // The version of the IDE or editor the LS is running in "automaticAuthentication": "true", // Whether LS will automatically authenticate on scan start (default: true) "deviceId": "a UUID", // A unique ID from the running the LS, used for telemetry - "filterSeverity": { // Filters to be applied for the determined issues + "filterSeverity": { // Optional filter to be applied for the determined issues (if omitted: no filtering) "critical": true, "high": true, "medium": true, "low": true, }, - "issueViewOptions": { // Another filter to be applied for the determined issues + "issueViewOptions": { // Optional filter to be applied for the determined issues (if omitted: no filtering) "openIssues": true, "ignoredIssues": false, }, diff --git a/application/config/config.go b/application/config/config.go index 1be5142bb..d5002fccb 100644 --- a/application/config/config.go +++ b/application/config/config.go @@ -585,29 +585,27 @@ func (c *Config) SetSnykAdvisorEnabled(enabled bool) { c.isSnykAdvisorEnabled = enabled } -func (c *Config) SetSeverityFilter(severityFilter types.SeverityFilter) bool { +func (c *Config) SetSeverityFilter(severityFilter *types.SeverityFilter) bool { c.m.Lock() defer c.m.Unlock() - emptySeverityFilter := types.SeverityFilter{} - if severityFilter == emptySeverityFilter { + if severityFilter == nil { return false } - filterModified := c.filterSeverity != severityFilter + filterModified := c.filterSeverity != *severityFilter c.logger.Debug().Str("method", "SetSeverityFilter").Interface("severityFilter", severityFilter).Msg("Setting severity filter:") - c.filterSeverity = severityFilter + c.filterSeverity = *severityFilter return filterModified } -func (c *Config) SetIssueViewOptions(issueViewOptions types.IssueViewOptions) bool { +func (c *Config) SetIssueViewOptions(issueViewOptions *types.IssueViewOptions) bool { c.m.Lock() defer c.m.Unlock() - emptyIssueViewOptions := types.IssueViewOptions{} - if issueViewOptions == emptyIssueViewOptions { + if issueViewOptions == nil { return false } - issueViewOptionsModified := c.issueViewOptions != issueViewOptions + issueViewOptionsModified := c.issueViewOptions != *issueViewOptions c.logger.Debug().Str("method", "SetIssueViewOptions").Interface("issueViewOptions", issueViewOptions).Msg("Setting issue view options:") - c.issueViewOptions = issueViewOptions + c.issueViewOptions = *issueViewOptions return issueViewOptionsModified } diff --git a/application/config/config_test.go b/application/config/config_test.go index ab7fb50e4..d38b23ead 100644 --- a/application/config/config_test.go +++ b/application/config/config_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/snyk/snyk-ls/internal/util" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -168,7 +170,7 @@ func TestSnykCodeApi(t *testing.T) { func Test_SetSeverityFilter(t *testing.T) { t.Run("Saves filter", func(t *testing.T) { c := New() - c.SetSeverityFilter(types.NewSeverityFilter(true, true, false, false)) + c.SetSeverityFilter(util.Ptr(types.NewSeverityFilter(true, true, false, false))) assert.Equal(t, types.NewSeverityFilter(true, true, false, false), c.FilterSeverity()) }) @@ -176,10 +178,10 @@ func Test_SetSeverityFilter(t *testing.T) { c := New() lowExcludedFilter := types.NewSeverityFilter(true, true, false, false) - modified := c.SetSeverityFilter(lowExcludedFilter) + modified := c.SetSeverityFilter(&lowExcludedFilter) assert.True(t, modified) - modified = c.SetSeverityFilter(lowExcludedFilter) + modified = c.SetSeverityFilter(&lowExcludedFilter) assert.False(t, modified) }) } @@ -187,7 +189,7 @@ func Test_SetSeverityFilter(t *testing.T) { func Test_SetIssueViewOptions(t *testing.T) { t.Run("Saves filter", func(t *testing.T) { c := New() - c.SetIssueViewOptions(types.NewIssueViewOptions(false, true)) + c.SetIssueViewOptions(util.Ptr(types.NewIssueViewOptions(false, true))) assert.Equal(t, types.NewIssueViewOptions(false, true), c.IssueViewOptions()) }) @@ -195,10 +197,10 @@ func Test_SetIssueViewOptions(t *testing.T) { c := New() ignoredOnlyFilter := types.NewIssueViewOptions(false, true) - modified := c.SetIssueViewOptions(ignoredOnlyFilter) + modified := c.SetIssueViewOptions(&ignoredOnlyFilter) assert.True(t, modified) - modified = c.SetIssueViewOptions(ignoredOnlyFilter) + modified = c.SetIssueViewOptions(&ignoredOnlyFilter) assert.False(t, modified) }) } diff --git a/application/server/authentication_smoke_test.go b/application/server/authentication_smoke_test.go index b5f6b418b..b6445ae13 100644 --- a/application/server/authentication_smoke_test.go +++ b/application/server/authentication_smoke_test.go @@ -23,6 +23,8 @@ import ( "testing" "time" + "github.com/snyk/snyk-ls/internal/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -78,8 +80,8 @@ func checkInvalidCredentialsMessageRequest(t *testing.T, expected string, tokenS InitializationOptions: types.Settings{ Token: tokenString, EnableTrustedFoldersFeature: "false", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), AuthenticationMethod: types.OAuthAuthentication, AutomaticAuthentication: "false", }, diff --git a/application/server/configuration.go b/application/server/configuration.go index 745227532..4ae2e0f4f 100644 --- a/application/server/configuration.go +++ b/application/server/configuration.go @@ -395,7 +395,7 @@ func updateProductEnablement(c *config.Config, settings types.Settings) { } } -func updateIssueViewOptions(c *config.Config, s types.IssueViewOptions) { +func updateIssueViewOptions(c *config.Config, s *types.IssueViewOptions) { c.Logger().Debug().Str("method", "updateIssueViewOptions").Interface("issueViewOptions", s).Msg("Updating issue view options:") modified := c.SetIssueViewOptions(s) @@ -404,7 +404,7 @@ func updateIssueViewOptions(c *config.Config, s types.IssueViewOptions) { } } -func updateSeverityFilter(c *config.Config, s types.SeverityFilter) { +func updateSeverityFilter(c *config.Config, s *types.SeverityFilter) { c.Logger().Debug().Str("method", "updateSeverityFilter").Interface("severityFilter", s).Msg("Updating severity filter:") modified := c.SetSeverityFilter(s) diff --git a/application/server/configuration_test.go b/application/server/configuration_test.go index 1578b0885..8b120402a 100644 --- a/application/server/configuration_test.go +++ b/application/server/configuration_test.go @@ -171,6 +171,8 @@ func Test_UpdateSettings(t *testing.T) { tempDir1 := filepath.Join(t.TempDir(), "tempDir1") tempDir2 := filepath.Join(t.TempDir(), "tempDir2") + nonDefaultSeverityFilter := types.NewSeverityFilter(false, true, false, true) + nonDefaultIssueViewOptions := types.NewIssueViewOptions(false, true) hoverVerbosity := 1 outputFormat := "html" settings := types.Settings{ @@ -187,8 +189,8 @@ func Test_UpdateSettings(t *testing.T) { ManageBinariesAutomatically: "false", CliPath: filepath.Join(t.TempDir(), "cli"), Token: "a fancy token", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: &nonDefaultSeverityFilter, + IssueViewOptions: &nonDefaultIssueViewOptions, TrustedFolders: []string{"trustedPath1", "trustedPath2"}, OsPlatform: "windows", OsArch: "amd64", @@ -234,8 +236,8 @@ func Test_UpdateSettings(t *testing.T) { assert.Equal(t, expectedOrgId, c.Organization()) assert.False(t, c.ManageBinariesAutomatically()) assert.Equal(t, settings.CliPath, c.CliSettings().Path()) - assert.Equal(t, types.DefaultSeverityFilter(), c.FilterSeverity()) - assert.Equal(t, types.DefaultIssueViewOptions(), c.IssueViewOptions()) + assert.Equal(t, nonDefaultSeverityFilter, c.FilterSeverity()) + assert.Equal(t, nonDefaultIssueViewOptions, c.IssueViewOptions()) assert.Subset(t, []types.FilePath{"trustedPath1", "trustedPath2"}, c.TrustedFolders()) assert.Equal(t, settings.OsPlatform, c.OsPlatform()) assert.Equal(t, settings.OsArch, c.OsArch()) @@ -405,18 +407,46 @@ func Test_UpdateSettings(t *testing.T) { c := testutil.UnitTest(t) t.Run("filtering gets passed", func(t *testing.T) { mixedSeverityFilter := types.NewSeverityFilter(true, false, true, false) - UpdateSettings(c, types.Settings{FilterSeverity: mixedSeverityFilter}) + UpdateSettings(c, types.Settings{FilterSeverity: &mixedSeverityFilter}) assert.Equal(t, mixedSeverityFilter, c.FilterSeverity()) }) + t.Run("equivalent of the \"empty\" struct as a filter gets passed", func(t *testing.T) { + emptyLikeSeverityFilter := types.NewSeverityFilter(false, false, false, false) + UpdateSettings(c, types.Settings{FilterSeverity: &emptyLikeSeverityFilter}) + + assert.Equal(t, emptyLikeSeverityFilter, c.FilterSeverity()) + }) + t.Run("omitting filter does not cause an update", func(t *testing.T) { + mixedSeverityFilter := types.NewSeverityFilter(false, false, true, false) + UpdateSettings(c, types.Settings{FilterSeverity: &mixedSeverityFilter}) + assert.Equal(t, mixedSeverityFilter, c.FilterSeverity()) + + UpdateSettings(c, types.Settings{}) + assert.Equal(t, mixedSeverityFilter, c.FilterSeverity()) + }) }) t.Run("issue view options", func(t *testing.T) { c := testutil.UnitTest(t) t.Run("filtering gets passed", func(t *testing.T) { mixedIssueViewOptions := types.NewIssueViewOptions(false, true) - UpdateSettings(c, types.Settings{IssueViewOptions: mixedIssueViewOptions}) + UpdateSettings(c, types.Settings{IssueViewOptions: &mixedIssueViewOptions}) + + assert.Equal(t, mixedIssueViewOptions, c.IssueViewOptions()) + }) + t.Run("equivalent of the \"empty\" struct as a filter gets passed", func(t *testing.T) { + emptyLikeIssueViewOptions := types.NewIssueViewOptions(false, false) + UpdateSettings(c, types.Settings{IssueViewOptions: &emptyLikeIssueViewOptions}) + + assert.Equal(t, emptyLikeIssueViewOptions, c.IssueViewOptions()) + }) + t.Run("omitting filter does not cause an update", func(t *testing.T) { + mixedIssueViewOptions := types.NewIssueViewOptions(false, true) + UpdateSettings(c, types.Settings{IssueViewOptions: &mixedIssueViewOptions}) + assert.Equal(t, mixedIssueViewOptions, c.IssueViewOptions()) + UpdateSettings(c, types.Settings{}) assert.Equal(t, mixedIssueViewOptions, c.IssueViewOptions()) }) }) diff --git a/application/server/parallelization_test.go b/application/server/parallelization_test.go index 56b848ec0..f56aaccf4 100644 --- a/application/server/parallelization_test.go +++ b/application/server/parallelization_test.go @@ -24,6 +24,8 @@ import ( "testing" "time" + "github.com/snyk/snyk-ls/internal/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -80,8 +82,8 @@ func Test_Concurrent_CLI_Runs(t *testing.T) { Endpoint: os.Getenv("SNYK_API"), Token: os.Getenv("SNYK_TOKEN"), EnableTrustedFoldersFeature: "false", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), AuthenticationMethod: types.TokenAuthentication, AutomaticAuthentication: "false", ManageBinariesAutomatically: "true", diff --git a/application/server/server_smoke_test.go b/application/server/server_smoke_test.go index 3473424e8..a3c332d92 100644 --- a/application/server/server_smoke_test.go +++ b/application/server/server_smoke_test.go @@ -27,6 +27,8 @@ import ( "testing" "time" + "github.com/snyk/snyk-ls/internal/util" + "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/server" "github.com/go-git/go-git/v5" @@ -793,8 +795,8 @@ func prepareInitParams(t *testing.T, cloneTargetDir types.FilePath, c *config.Co Endpoint: os.Getenv("SNYK_API"), Token: os.Getenv("SNYK_TOKEN"), EnableTrustedFoldersFeature: "false", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), AuthenticationMethod: types.TokenAuthentication, EnableDeltaFindings: strconv.FormatBool(c.IsDeltaFindingsEnabled()), ActivateSnykCode: strconv.FormatBool(c.IsSnykCodeEnabled()), @@ -867,8 +869,8 @@ func Test_SmokeSnykCodeFileScan(t *testing.T) { Endpoint: os.Getenv("SNYK_API"), Token: os.Getenv("SNYK_TOKEN"), EnableTrustedFoldersFeature: "false", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), }, } diff --git a/application/server/server_test.go b/application/server/server_test.go index 140154df0..3e4251ee0 100644 --- a/application/server/server_test.go +++ b/application/server/server_test.go @@ -25,6 +25,8 @@ import ( "testing" "time" + "github.com/snyk/snyk-ls/internal/util" + "github.com/snyk/snyk-ls/domain/snyk" "github.com/snyk/snyk-ls/domain/snyk/scanner" "github.com/snyk/snyk-ls/internal/storedconfig" @@ -367,8 +369,8 @@ func Test_TextDocumentCodeLenses_shouldReturnCodeLenses(t *testing.T) { Token: "xxx", ManageBinariesAutomatically: "true", CliPath: filepath.Join(t.TempDir(), "cli"), - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), EnableTrustedFoldersFeature: "false", }, } @@ -430,8 +432,8 @@ func Test_TextDocumentCodeLenses_dirtyFileShouldFilterCodeLenses(t *testing.T) { Token: "xxx", ManageBinariesAutomatically: "true", CliPath: filepath.Join(t.TempDir(), "cli"), - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), EnableTrustedFoldersFeature: "false", }, } @@ -486,8 +488,8 @@ func Test_initialize_updatesSettings(t *testing.T) { InitializationOptions: types.Settings{ Organization: expectedOrgId, Token: "xxx", - FilterSeverity: types.DefaultSeverityFilter(), - IssueViewOptions: types.DefaultIssueViewOptions(), + FilterSeverity: util.Ptr(types.DefaultSeverityFilter()), + IssueViewOptions: util.Ptr(types.DefaultIssueViewOptions()), }, } diff --git a/domain/ide/workspace/folder_test.go b/domain/ide/workspace/folder_test.go index 14aa15056..1570e658b 100644 --- a/domain/ide/workspace/folder_test.go +++ b/domain/ide/workspace/folder_test.go @@ -201,7 +201,7 @@ func TestProcessResults_whenFilteringSeverity_ProcessesOnlyFilteredIssues(t *tes c := testutil.UnitTest(t) severityFilter := types.NewSeverityFilter(true, false, true, false) - config.CurrentConfig().SetSeverityFilter(severityFilter) + config.CurrentConfig().SetSeverityFilter(&severityFilter) f := NewMockFolder(c, notification.NewNotifier()) @@ -251,7 +251,7 @@ func TestProcessResults_whenFilteringIssueViewOptions_ProcessesOnlyFilteredIssue c := testutil.UnitTest(t) issueViewOptions := types.NewIssueViewOptions(false, true) - config.CurrentConfig().SetIssueViewOptions(issueViewOptions) + config.CurrentConfig().SetIssueViewOptions(&issueViewOptions) f := NewMockFolder(c, notification.NewNotifier()) @@ -435,7 +435,7 @@ func Test_FilterCachedDiagnostics_filtersDisabledSeverity(t *testing.T) { f := NewFolder(c, folderPath, "Test", scannerRecorder, hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notification.NewMockNotifier(), persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator()) ctx := context.Background() - c.SetSeverityFilter(types.NewSeverityFilter(true, true, false, false)) + c.SetSeverityFilter(util.Ptr(types.NewSeverityFilter(true, true, false, false))) // act f.ScanFile(ctx, filePath) @@ -489,8 +489,8 @@ func Test_FilterCachedDiagnostics_filtersIgnoredIssues(t *testing.T) { f := NewFolder(c, folderPath, "Test", scannerRecorder, hover.NewFakeHoverService(), scanner.NewMockScanNotifier(), notification.NewMockNotifier(), persistence.NewNopScanPersister(), scanstates.NewNoopStateAggregator()) ctx := context.Background() - c.SetSeverityFilter(types.NewSeverityFilter(true, true, false, false)) - c.SetIssueViewOptions(types.NewIssueViewOptions(true, false)) + c.SetSeverityFilter(util.Ptr(types.NewSeverityFilter(true, true, false, false))) + c.SetIssueViewOptions(util.Ptr(types.NewIssueViewOptions(true, false))) // act f.ScanFile(ctx, filePath) diff --git a/internal/testutil/test_setup.go b/internal/testutil/test_setup.go index dbe933578..f36ede99b 100644 --- a/internal/testutil/test_setup.go +++ b/internal/testutil/test_setup.go @@ -22,6 +22,8 @@ import ( "path/filepath" "testing" + "github.com/snyk/snyk-ls/internal/util" + "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/mocks" "github.com/stretchr/testify/require" @@ -124,7 +126,7 @@ func prepareTestHelper(t *testing.T, envVar string, useConsistentIgnores bool) * c.SetAuthenticationMethod(types.TokenAuthentication) c.SetErrorReportingEnabled(false) c.SetTrustedFolderFeatureEnabled(false) - c.SetIssueViewOptions(types.IssueViewOptions{OpenIssues: true, IgnoredIssues: true}) + c.SetIssueViewOptions(util.Ptr(types.NewIssueViewOptions(true, true))) setMCPServerURL(t, c) redirectConfigAndDataHome(t, c) diff --git a/internal/types/lsp.go b/internal/types/lsp.go index 9b2060e5a..5c182e4f1 100644 --- a/internal/types/lsp.go +++ b/internal/types/lsp.go @@ -581,8 +581,8 @@ type Settings struct { IntegrationVersion string `json:"integrationVersion,omitempty"` AutomaticAuthentication string `json:"automaticAuthentication,omitempty"` DeviceId string `json:"deviceId,omitempty"` - FilterSeverity SeverityFilter `json:"filterSeverity,omitempty"` - IssueViewOptions IssueViewOptions `json:"issueViewOptions,omitempty"` + FilterSeverity *SeverityFilter `json:"filterSeverity,omitempty"` + IssueViewOptions *IssueViewOptions `json:"issueViewOptions,omitempty"` SendErrorReports string `json:"sendErrorReports,omitempty"` ManageBinariesAutomatically string `json:"manageBinariesAutomatically,omitempty"` EnableTrustedFoldersFeature string `json:"enableTrustedFoldersFeature,omitempty"` diff --git a/internal/util/ptr.go b/internal/util/ptr.go new file mode 100644 index 000000000..cbbf718c7 --- /dev/null +++ b/internal/util/ptr.go @@ -0,0 +1,24 @@ +/* + * © 2025 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 util + +// Ptr returns a pointer to the input value. +// Because in Go you can't do something like `takesPtr(&(returnsStruct()))`. +// So instead do `takesPtr(Ptr(returnsStruct()))`. +func Ptr[T any](v T) *T { + return &v // Go's escape analysis will make this a heap pointer. +}