diff --git a/handlers/aliases/lsp/text-document-signature-help.go b/handlers/aliases/lsp/text-document-signature-help.go index ce3a9b0..6408f50 100644 --- a/handlers/aliases/lsp/text-document-signature-help.go +++ b/handlers/aliases/lsp/text-document-signature-help.go @@ -1,6 +1,7 @@ package lsp import ( + "config-lsp/common" "config-lsp/handlers/aliases" "config-lsp/handlers/aliases/ast" "config-lsp/handlers/aliases/handlers" @@ -13,7 +14,7 @@ func TextDocumentSignatureHelp(context *glsp.Context, params *protocol.Signature document := aliases.DocumentParserMap[params.TextDocument.URI] line := params.Position.Line - character := params.Position.Character + character := common.CursorToCharacterIndex(params.Position.Character) if _, found := document.Parser.CommentLines[line]; found { // Comment diff --git a/handlers/sshd_config/Config.g4 b/handlers/sshd_config/Config.g4 index 569a6a8..ad4cbe8 100644 --- a/handlers/sshd_config/Config.g4 +++ b/handlers/sshd_config/Config.g4 @@ -1,7 +1,7 @@ grammar Config; lineStatement - : (entry | (leadingComment) | WHITESPACE?) EOF + : (entry | leadingComment | WHITESPACE?) EOF ; entry diff --git a/handlers/sshd_config/analyzer/analyzer.go b/handlers/sshd_config/analyzer/analyzer.go index 0b661c6..36944aa 100644 --- a/handlers/sshd_config/analyzer/analyzer.go +++ b/handlers/sshd_config/analyzer/analyzer.go @@ -49,6 +49,8 @@ func Analyze( } } + errors = append(errors, analyzeMatchBlocks(d)...) + if len(errors) > 0 { return errsToDiagnostics(errors) } diff --git a/handlers/sshd_config/analyzer/match.go b/handlers/sshd_config/analyzer/match.go new file mode 100644 index 0000000..c5ad800 --- /dev/null +++ b/handlers/sshd_config/analyzer/match.go @@ -0,0 +1,112 @@ +package analyzer + +import ( + "config-lsp/common" + sshdconfig "config-lsp/handlers/sshd_config" + match_parser "config-lsp/handlers/sshd_config/fields/match-parser" + "config-lsp/utils" + "errors" + "fmt" + "strings" +) + +func analyzeMatchBlocks( + d *sshdconfig.SSHDocument, +) []common.LSPError { + errs := make([]common.LSPError, 0) + + for _, indexOption := range d.Indexes.AllOptionsPerName["Match"] { + matchBlock := indexOption.MatchBlock.MatchValue + + // Check if the match block has filled out all fields + if matchBlock == nil || len(matchBlock.Entries) == 0 { + errs = append(errs, common.LSPError{ + Range: indexOption.Option.LocationRange, + Err: errors.New("A match expression is required"), + }) + continue + } + + for _, entry := range matchBlock.Entries { + if entry.Values == nil { + errs = append(errs, common.LSPError{ + Range: entry.LocationRange, + Err: errors.New(fmt.Sprintf("A value for %s is required", entry.Criteria.Type)), + }) + } else { + errs = append(errs, analyzeMatchValuesContainsPositiveValue(entry.Values)...) + + for _, value := range entry.Values.Values { + errs = append(errs, analyzeMatchValueNegation(value)...) + } + } + } + + // Check if match blocks are not empty + if indexOption.MatchBlock.Options.Size() == 0 { + errs = append(errs, common.LSPError{ + Range: indexOption.Option.LocationRange, + Err: errors.New("This match block is empty"), + }) + } + } + + return errs +} + +func analyzeMatchValueNegation( + value *match_parser.MatchValue, +) []common.LSPError { + errs := make([]common.LSPError, 0) + + positionsAsList := utils.AllIndexes(value.Value, "!") + positions := utils.SliceToMap(positionsAsList, struct{}{}) + + delete(positions, 0) + + for position := range positions { + errs = append(errs, common.LSPError{ + Range: common.LocationRange{ + Start: common.Location{ + Line: value.Start.Line, + Character: uint32(position) + value.Start.Character, + }, + End: common.Location{ + Line: value.End.Line, + Character: uint32(position) + value.End.Character, + }, + }, + Err: errors.New("The negation operator (!) may only occur at the beginning of a value"), + }) + } + + return errs +} + +func analyzeMatchValuesContainsPositiveValue( + values *match_parser.MatchValues, +) []common.LSPError { + if len(values.Values) == 0 { + return nil + } + + containsPositive := false + + for _, value := range values.Values { + if !strings.HasPrefix(value.Value, "!") { + containsPositive = true + break + } + } + + if !containsPositive { + return []common.LSPError{ + { + Range: values.LocationRange, + Err: errors.New("At least one positive value is required. A negated match will never produce a positive result by itself"), + }, + } + } + + return nil +} diff --git a/handlers/sshd_config/analyzer/match_test.go b/handlers/sshd_config/analyzer/match_test.go new file mode 100644 index 0000000..b65b478 --- /dev/null +++ b/handlers/sshd_config/analyzer/match_test.go @@ -0,0 +1,63 @@ +package analyzer + +import ( + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/ast" + "config-lsp/handlers/sshd_config/indexes" + "config-lsp/utils" + "testing" +) + +func TestEmptyMatchBlocksMakesErrors( + t *testing.T, +) { + input := utils.Dedent(` +PermitRootLogin yes +Match User root +`) + c := ast.NewSSHConfig() + errors := c.Parse(input) + + if len(errors) > 0 { + t.Fatalf("Parse error: %v", errors) + } + + indexes, errors := indexes.CreateIndexes(*c) + + if len(errors) > 0 { + t.Fatalf("Index error: %v", errors) + } + + d := &sshdconfig.SSHDocument{ + Config: c, + Indexes: indexes, + } + + errors = analyzeMatchBlocks(d) + + if !(len(errors) == 1) { + t.Errorf("Expected 1 error, got %v", len(errors)) + } +} + +func TestContainsOnlyNegativeValues( + t *testing.T, +) { + input := utils.Dedent(` +PermitRootLogin yes +Match User !root,!admin +`) + c := ast.NewSSHConfig() + errors := c.Parse(input) + + if len(errors) > 0 { + t.Fatalf("Parse error: %v", errors) + } + + _, matchBlock := c.FindOption(uint32(1)) + errors = analyzeMatchValuesContainsPositiveValue(matchBlock.MatchValue.Entries[0].Values) + + if !(len(errors) == 1) { + t.Errorf("Expected 1 error, got %v", len(errors)) + } +} diff --git a/handlers/sshd_config/ast/listener.go b/handlers/sshd_config/ast/listener.go index b5c81b2..4830005 100644 --- a/handlers/sshd_config/ast/listener.go +++ b/handlers/sshd_config/ast/listener.go @@ -93,6 +93,10 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) { location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext) location.ChangeBothLines(s.sshContext.line) + defer (func() { + s.sshContext.currentOption = nil + })() + if s.sshContext.isKeyAMatchBlock { // Add new match block var match *match_parser.Match @@ -131,17 +135,20 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) { s.sshContext.currentMatchBlock = matchBlock s.sshContext.isKeyAMatchBlock = false - } else if s.sshContext.currentMatchBlock != nil { + + return + } + + if s.sshContext.currentMatchBlock != nil { s.sshContext.currentMatchBlock.Options.Put( location.Start.Line, s.sshContext.currentOption, ) + s.sshContext.currentMatchBlock.End = s.sshContext.currentOption.End } else { s.Config.Options.Put( location.Start.Line, s.sshContext.currentOption, ) } - - s.sshContext.currentOption = nil } diff --git a/handlers/sshd_config/ast/parser_test.go b/handlers/sshd_config/ast/parser_test.go index e883966..63d5bb0 100644 --- a/handlers/sshd_config/ast/parser_test.go +++ b/handlers/sshd_config/ast/parser_test.go @@ -92,6 +92,10 @@ Match Address 192.168.0.1 t.Errorf("Expected second entry to be 'Match Address 192.168.0.1', but got: %v", secondEntry.MatchEntry.Value) } + if !(secondEntry.Start.Line == 2 && secondEntry.Start.Character == 0 && secondEntry.End.Line == 3 && secondEntry.End.Character == 26) { + t.Errorf("Expected second entry's location to be 2:0-3:25, but got: %v", secondEntry.LocationRange) + } + if !(secondEntry.MatchValue.Entries[0].Criteria.Type == "Address" && secondEntry.MatchValue.Entries[0].Values.Values[0].Value == "192.168.0.1" && secondEntry.MatchEntry.OptionValue.Start.Character == 6) { t.Errorf("Expected second entry to be 'Match Address 192.168.0.1', but got: %v", secondEntry.MatchValue) } @@ -235,6 +239,17 @@ Match Address 192.168.0.2 if !(matchOption.Value == "Match User lena" && matchBlock.MatchEntry.Value == "Match User lena" && matchBlock.MatchValue.Entries[0].Values.Values[0].Value == "lena" && matchBlock.MatchEntry.OptionValue.Start.Character == 6) { t.Errorf("Expected match option to be 'Match User lena', but got: %v, %v", matchOption, matchBlock) } + + if !(matchOption.Start.Line == 2 && matchOption.End.Line == 2 && matchOption.Start.Character == 0 && matchOption.End.Character == 14) { + t.Errorf("Expected match option to be at 2:0-14, but got: %v", matchOption.LocationRange) + } + + if !(matchBlock.Start.Line == 2 && + matchBlock.Start.Character == 0 && + matchBlock.End.Line == 4 && + matchBlock.End.Character == 20) { + t.Errorf("Expected match block to be at 2:0-4:20, but got: %v", matchBlock.LocationRange) + } } func TestSimpleExampleWithComments( diff --git a/handlers/sshd_config/handlers/completions_match.go b/handlers/sshd_config/handlers/completions_match.go index 4a6fa2f..6a10ebe 100644 --- a/handlers/sshd_config/handlers/completions_match.go +++ b/handlers/sshd_config/handlers/completions_match.go @@ -13,7 +13,7 @@ func getMatchCompletions( match *match_parser.Match, cursor uint32, ) ([]protocol.CompletionItem, error) { - if len(match.Entries) == 0 { + if match == nil || len(match.Entries) == 0 { completions := getMatchCriteriaCompletions() completions = append(completions, getMatchAllKeywordCompletion()) diff --git a/handlers/sshd_config/indexes/indexes.go b/handlers/sshd_config/indexes/indexes.go index ed56afd..ebdda30 100644 --- a/handlers/sshd_config/indexes/indexes.go +++ b/handlers/sshd_config/indexes/indexes.go @@ -82,6 +82,8 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) { case *ast.SSHMatchBlock: matchBlock := entry.(*ast.SSHMatchBlock) + errs = append(errs, addOption(indexes, matchBlock.MatchEntry, matchBlock)...) + it := matchBlock.Options.Iterator() for it.Next() { option := it.Value().(*ast.SSHOption) diff --git a/root-handler/handler.go b/root-handler/handler.go index 4a7cd3c..fc59b8a 100644 --- a/root-handler/handler.go +++ b/root-handler/handler.go @@ -42,15 +42,7 @@ func SetUpRootHandler() { func initialize(context *glsp.Context, params *protocol.InitializeParams) (any, error) { capabilities := lspHandler.CreateServerCapabilities() capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull - capabilities.SignatureHelpProvider = &protocol.SignatureHelpOptions{ - TriggerCharacters: []string{ - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "_", "-", ".", "/", ":", "@", "#", "!", "$", "%", "^", "&", "*", "(", ")", "+", "=", "[", "]", "{", "}", "<", ">", "?", ";", ",", "|", - " ", - }, - } + capabilities.SignatureHelpProvider = &protocol.SignatureHelpOptions{} if (*params.Capabilities.TextDocument.Rename.PrepareSupport) == true { // Client supports rename preparation diff --git a/utils/strings.go b/utils/strings.go index bc32f88..3f7f851 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -2,6 +2,7 @@ package utils import ( "regexp" + "strings" ) var trimIndexPattern = regexp.MustCompile(`^\s*(.+?)\s*$`) @@ -57,3 +58,21 @@ var emptyRegex = regexp.MustCompile(`^\s*$`) func IsEmpty(s string) bool { return emptyRegex.MatchString(s) } + +func AllIndexes(s string, sub string) []int { + indexes := make([]int, 0) + current := s + + for { + index := strings.Index(current, sub) + + if index == -1 { + break + } + + indexes = append(indexes, index) + current = current[index+1:] + } + + return indexes +}