diff --git a/server/handlers/ssh_config/analyzer/analyzer.go b/server/handlers/ssh_config/analyzer/analyzer.go index 31b8d2f..447bb34 100644 --- a/server/handlers/ssh_config/analyzer/analyzer.go +++ b/server/handlers/ssh_config/analyzer/analyzer.go @@ -55,6 +55,7 @@ func Analyze( } analyzeValuesAreValid(ctx) + analyzeTokens(ctx) analyzeIgnoreUnknownHasNoUnnecessary(ctx) analyzeDependents(ctx) analyzeBlocks(ctx) diff --git a/server/handlers/ssh_config/analyzer/include.go b/server/handlers/ssh_config/analyzer/include.go index f833b98..3be60df 100644 --- a/server/handlers/ssh_config/analyzer/include.go +++ b/server/handlers/ssh_config/analyzer/include.go @@ -4,6 +4,7 @@ import ( "config-lsp/common" sshconfig "config-lsp/handlers/ssh_config" "config-lsp/handlers/ssh_config/ast" + "config-lsp/handlers/ssh_config/fields" "config-lsp/handlers/ssh_config/indexes" "config-lsp/utils" "errors" @@ -19,27 +20,6 @@ import ( var whitespacePattern = regexp.MustCompile(`\S+`) var environmtalVariablePattern = regexp.MustCompile(`\${.+?}`) -var availableTokens = []string{ - "%%", - "%C", - "%d", - "%f", - "%H", - "%h", - "%l", - "%i", - "%j", - "%K", - "%k", - "%L", - "%l", - "%n", - "%p", - "%r", - "%T", - "%t", - "%u", -} func analyzeIncludeValues( ctx *analyzerContext, @@ -74,7 +54,7 @@ func isImpossibleToVerify( return true } - for _, token := range availableTokens { + for _, token := range fields.AvailableTokens { if strings.Contains(path, token) { return true } diff --git a/server/handlers/ssh_config/analyzer/tokens.go b/server/handlers/ssh_config/analyzer/tokens.go new file mode 100644 index 0000000..58341bd --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tokens.go @@ -0,0 +1,49 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/ssh_config/fields" + "config-lsp/utils" + "fmt" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeTokens( + ctx *analyzerContext, +) { + for _, info := range ctx.document.Config.GetAllOptions() { + if info.Option.Key == nil || info.Option.OptionValue == nil { + continue + } + + key := info.Option.Key.Key + text := info.Option.OptionValue.Value.Value + var tokens []string + + if foundTokens, found := fields.OptionsTokensMap[key]; found { + tokens = foundTokens + } else { + tokens = []string{} + } + + disallowedTokens := utils.Without(fields.AvailableTokens, tokens) + + for _, token := range disallowedTokens { + if strings.Contains(text, token) { + optionName := string(key) + + if formatted, found := fields.FieldsNameFormattedMap[key]; found { + optionName = formatted + } + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: info.Option.OptionValue.ToLSPRange(), + Message: fmt.Sprintf("Token '%s' is not allowed for option '%s'", token, optionName), + Severity: &common.SeverityError, + }) + } + } + } +} diff --git a/server/handlers/ssh_config/analyzer/tokens_test.go b/server/handlers/ssh_config/analyzer/tokens_test.go new file mode 100644 index 0000000..cd2358e --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tokens_test.go @@ -0,0 +1,62 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/ssh_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestInvalidTokensForNonExisting( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist Hello%%World +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} + +func TestInvalidTokensForExistingOption( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Tunnel Hello%%World +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} + +func TestValidTokens( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +LocalCommand Hello World %% and %d +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if len(ctx.diagnostics) > 0 { + t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) + } +} diff --git a/server/handlers/ssh_config/fields/tokens.go b/server/handlers/ssh_config/fields/tokens.go new file mode 100644 index 0000000..2fb69d1 --- /dev/null +++ b/server/handlers/ssh_config/fields/tokens.go @@ -0,0 +1,71 @@ +package fields + +var AvailableTokens = []string{ + "%%", + "%C", + "%d", + "%f", + "%H", + "%h", + "%l", + "%i", + "%j", + "%K", + "%k", + "%L", + "%l", + "%n", + "%p", + "%r", + "%T", + "%t", + "%u", +} + +// A map of