diff --git a/server/handlers/sshd_config/analyzer/analyzer.go b/server/handlers/sshd_config/analyzer/analyzer.go index 0007b09..72cb52e 100644 --- a/server/handlers/sshd_config/analyzer/analyzer.go +++ b/server/handlers/sshd_config/analyzer/analyzer.go @@ -54,6 +54,7 @@ func Analyze( } } + analyzeValuesAreValid(ctx) analyzeMatchBlocks(ctx) analyzeTokens(ctx) diff --git a/server/handlers/sshd_config/analyzer/quotes.go b/server/handlers/sshd_config/analyzer/quotes.go index a563bc5..e423510 100644 --- a/server/handlers/sshd_config/analyzer/quotes.go +++ b/server/handlers/sshd_config/analyzer/quotes.go @@ -11,12 +11,12 @@ import ( func analyzeQuotesAreValid( ctx *analyzerContext, ) { - for _, option := range ctx.document.Config.GetAllOptions() { - checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange) - checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) + for _, info := range ctx.document.Config.GetAllOptions() { + checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) - checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange) - checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) + checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) } } diff --git a/server/handlers/sshd_config/analyzer/tokens.go b/server/handlers/sshd_config/analyzer/tokens.go index ee642db..4c29b2d 100644 --- a/server/handlers/sshd_config/analyzer/tokens.go +++ b/server/handlers/sshd_config/analyzer/tokens.go @@ -13,13 +13,13 @@ import ( func analyzeTokens( ctx *analyzerContext, ) { - for _, option := range ctx.document.Config.GetAllOptions() { - if option.Key == nil || option.OptionValue == nil { + for _, info := range ctx.document.Config.GetAllOptions() { + if info.Option.Key == nil || info.Option.OptionValue == nil { continue } - key := option.Key.Key - text := option.OptionValue.Value.Value + key := info.Option.Key.Key + text := info.Option.OptionValue.Value.Value var tokens []string if foundTokens, found := fields.OptionsTokensMap[key]; found { @@ -39,7 +39,7 @@ func analyzeTokens( } ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.OptionValue.ToLSPRange(), + 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/sshd_config/analyzer/values.go b/server/handlers/sshd_config/analyzer/values.go new file mode 100644 index 0000000..d29d8c7 --- /dev/null +++ b/server/handlers/sshd_config/analyzer/values.go @@ -0,0 +1,39 @@ +package analyzer + +import ( + "config-lsp/handlers/sshd_config/diagnostics" + "config-lsp/handlers/sshd_config/fields" +) + +func analyzeValuesAreValid( + ctx *analyzerContext, +) { + // Check if there are unknown options + for _, info := range ctx.document.Config.GetAllOptions() { + normalizedName := fields.CreateNormalizedName(info.Option.Key.Value.Value) + + var isUnknown bool = true + + // Check if the option is unknown + if info.MatchBlock == nil { + // All options are allowed + if _, found := fields.Options[normalizedName]; found { + isUnknown = false + } + } else { + // Only `MatchAllowedOptions` are allowed + if _, found := fields.MatchAllowedOptions[normalizedName]; found { + isUnknown = false + } + } + + if isUnknown { + ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption( + info.Option.Key.ToLSPRange(), + info.Option.Key.Value.Value, + )) + + ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info + } + } +} diff --git a/server/handlers/sshd_config/analyzer/values_test.go b/server/handlers/sshd_config/analyzer/values_test.go new file mode 100644 index 0000000..b09df8c --- /dev/null +++ b/server/handlers/sshd_config/analyzer/values_test.go @@ -0,0 +1,34 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/sshd_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestUnknownOptionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeValuesAreValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} diff --git a/server/handlers/sshd_config/ast/sshd_config_ast_utils.go b/server/handlers/sshd_config/ast/sshd_config_ast_utils.go new file mode 100644 index 0000000..aeebea8 --- /dev/null +++ b/server/handlers/sshd_config/ast/sshd_config_ast_utils.go @@ -0,0 +1,8 @@ +// Contains structs that are used as utilities, but are +// not used for the AST itself +package ast + +type SSHDOptionInfo struct { + MatchBlock *SSHDMatchBlock + Option *SSHDOption +} diff --git a/server/handlers/sshd_config/ast/sshd_config_fields.go b/server/handlers/sshd_config/ast/sshd_config_fields.go index 11546d4..c5960fc 100644 --- a/server/handlers/sshd_config/ast/sshd_config_fields.go +++ b/server/handlers/sshd_config/ast/sshd_config_fields.go @@ -64,26 +64,32 @@ func (c SSHDConfig) FindOption(line uint32) (*SSHDOption, *SSHDMatchBlock) { return nil, nil } -func (c SSHDConfig) GetAllOptions() []*SSHDOption { - options := make( - []*SSHDOption, +func (c SSHDConfig) GetAllOptions() []SSHDOptionInfo { + infos := make( + []SSHDOptionInfo, 0, // Approximation, this does not need to be exact c.Options.Size()+10, ) + var currentMatchBlock *SSHDMatchBlock = nil + for _, rawEntry := range c.Options.Values() { switch entry := rawEntry.(type) { case *SSHDOption: - options = append(options, entry) + infos = append(infos, SSHDOptionInfo{ + Option: entry, + MatchBlock: currentMatchBlock, + }) case *SSHDMatchBlock: - options = append(options, entry.MatchOption) + currentMatchBlock = entry - for _, rawOption := range entry.Options.Values() { - options = append(options, rawOption.(*SSHDOption)) - } + infos = append(infos, SSHDOptionInfo{ + Option: entry.MatchOption, + MatchBlock: currentMatchBlock, + }) } } - return options + return infos } diff --git a/server/handlers/sshd_config/diagnostics/diagnostics.go b/server/handlers/sshd_config/diagnostics/diagnostics.go new file mode 100644 index 0000000..86b96f0 --- /dev/null +++ b/server/handlers/sshd_config/diagnostics/diagnostics.go @@ -0,0 +1,19 @@ +package diagnostics + +import ( + "config-lsp/common" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func GenerateUnknownOption( + diagnosticRange protocol.Range, + optionName string, +) protocol.Diagnostic { + return protocol.Diagnostic{ + Range: diagnosticRange, + Message: fmt.Sprintf("Unknown option: %s", optionName), + Severity: &common.SeverityError, + } +} diff --git a/server/handlers/sshd_config/indexes/indexes.go b/server/handlers/sshd_config/indexes/indexes.go index 236b359..656f36d 100644 --- a/server/handlers/sshd_config/indexes/indexes.go +++ b/server/handlers/sshd_config/indexes/indexes.go @@ -37,4 +37,6 @@ type SSHDIndexes struct { AllOptionsPerName map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption)) Includes map[uint32]*SSHDIndexIncludeLine + + UnknownOptions map[uint32]ast.SSHDOptionInfo } diff --git a/server/handlers/sshd_config/indexes/indexes_handlers.go b/server/handlers/sshd_config/indexes/indexes_handlers.go index 5c90047..76d2cfb 100644 --- a/server/handlers/sshd_config/indexes/indexes_handlers.go +++ b/server/handlers/sshd_config/indexes/indexes_handlers.go @@ -18,6 +18,7 @@ func CreateIndexes(config ast.SSHDConfig) (*SSHDIndexes, []common.LSPError) { indexes := &SSHDIndexes{ AllOptionsPerName: make(map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))), Includes: make(map[uint32]*SSHDIndexIncludeLine), + UnknownOptions: make(map[uint32]ast.SSHDOptionInfo), } it := config.Options.Iterator()