From e19ad01fd8ab024a90a4bb238bf50e8335c1f70a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:35:15 +0200 Subject: [PATCH] feat(ssh_config): Add analyzer; Overall improvements --- common/errors.go | 11 ++++ handlers/ssh_config/analyzer/analyzer.go | 23 +++++++- handlers/ssh_config/analyzer/dependents.go | 55 +++++++++++++++++++ handlers/ssh_config/analyzer/options.go | 18 +++--- handlers/ssh_config/analyzer/quotes_test.go | 33 +++++++++++ .../ssh_config/ast/ssh_cofig_fields_test.go | 7 +++ handlers/ssh_config/ast/ssh_config_fields.go | 14 ++++- handlers/ssh_config/document_fields.go | 7 +++ handlers/ssh_config/fields/options.go | 2 + handlers/ssh_config/handlers/completions.go | 6 +- 10 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 handlers/ssh_config/analyzer/dependents.go diff --git a/common/errors.go b/common/errors.go index 57fc0b9..bccacea 100644 --- a/common/errors.go +++ b/common/errors.go @@ -1,6 +1,8 @@ package common import ( + "config-lsp/utils" + protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -30,3 +32,12 @@ type SyntaxError struct { func (s SyntaxError) Error() string { return s.Message } + +func ErrsToDiagnostics(errs []LSPError) []protocol.Diagnostic { + return utils.Map( + errs, + func(err LSPError) protocol.Diagnostic { + return err.ToDiagnostic() + }, + ) +} diff --git a/handlers/ssh_config/analyzer/analyzer.go b/handlers/ssh_config/analyzer/analyzer.go index dd789d0..6d5ca09 100644 --- a/handlers/ssh_config/analyzer/analyzer.go +++ b/handlers/ssh_config/analyzer/analyzer.go @@ -1,12 +1,33 @@ package analyzer import ( + "config-lsp/common" sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/indexes" + protocol "github.com/tliron/glsp/protocol_3_16" ) func Analyze( d *sshconfig.SSHDocument, ) []protocol.Diagnostic { - return nil + errors := analyzeStructureIsValid(d) + + if len(errors) > 0 { + return common.ErrsToDiagnostics(errors) + } + + i, indexErrors := indexes.CreateIndexes(*d.Config) + + d.Indexes = i + + errors = append(errors, indexErrors...) + + if len(errors) > 0 { + return common.ErrsToDiagnostics(errors) + } + + errors = append(errors, analyzeDependents(d)...) + + return common.ErrsToDiagnostics(errors) } diff --git a/handlers/ssh_config/analyzer/dependents.go b/handlers/ssh_config/analyzer/dependents.go new file mode 100644 index 0000000..f9e3e9a --- /dev/null +++ b/handlers/ssh_config/analyzer/dependents.go @@ -0,0 +1,55 @@ +package analyzer + +import ( + "config-lsp/common" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/ast" + "config-lsp/handlers/ssh_config/fields" + "errors" + "fmt" +) + +func analyzeDependents( + d *sshconfig.SSHDocument, +) []common.LSPError { + errs := make([]common.LSPError, 0) + + for _, option := range d.Config.GetAllOptions() { + errs = append(errs, checkIsDependent(d, option.Option.Key, option.Block)...) + } + + return errs +} + +func checkIsDependent( + d *sshconfig.SSHDocument, + key *ast.SSHKey, + block ast.SSHBlock, +) []common.LSPError { + errs := make([]common.LSPError, 0) + + dependentOptions, found := fields.DependentFields[key.Key] + + if !found { + return errs + } + + for _, dependentOption := range dependentOptions { + if opts, found := d.Indexes.AllOptionsPerName[dependentOption]; found { + _, existsInBlock := opts[block] + _, existsInGlobal := opts[nil] + + if existsInBlock || existsInGlobal { + continue + } + } + + errs = append(errs, common.LSPError{ + Range: key.LocationRange, + Err: errors.New(fmt.Sprintf("Option '%s' requires option '%s' to be present", key.Key, dependentOption)), + }) + } + + return errs +} + diff --git a/handlers/ssh_config/analyzer/options.go b/handlers/ssh_config/analyzer/options.go index 3acd2b3..2f00ec3 100644 --- a/handlers/ssh_config/analyzer/options.go +++ b/handlers/ssh_config/analyzer/options.go @@ -22,13 +22,13 @@ func analyzeStructureIsValid( switch entry.(type) { case *ast.SSHOption: - errs = append(errs, checkOption(entry.(*ast.SSHOption), nil)...) + errs = append(errs, checkOption(d, entry.(*ast.SSHOption), nil)...) case *ast.SSHMatchBlock: matchBlock := entry.(*ast.SSHMatchBlock) - errs = append(errs, checkMatchBlock(matchBlock)...) + errs = append(errs, checkMatchBlock(d, matchBlock)...) case *ast.SSHHostBlock: hostBlock := entry.(*ast.SSHHostBlock) - errs = append(errs, checkHostBlock(hostBlock)...) + errs = append(errs, checkHostBlock(d, hostBlock)...) } } @@ -37,6 +37,7 @@ func analyzeStructureIsValid( } func checkOption( + d *sshconfig.SSHDocument, option *ast.SSHOption, block ast.SSHBlock, ) []common.LSPError { @@ -78,13 +79,10 @@ func checkOption( } else { errs = append(errs, checkIsUsingDoubleQuotes(option.OptionValue.Value, option.OptionValue.LocationRange)...) errs = append(errs, checkQuotesAreClosed(option.OptionValue.Value, option.OptionValue.LocationRange)...) - - invalidValues := docOption.CheckIsValid(option.OptionValue.Value.Value) - errs = append( errs, utils.Map( - invalidValues, + docOption.CheckIsValid(option.OptionValue.Value.Value), func(invalidValue *docvalues.InvalidValue) common.LSPError { err := docvalues.LSPErrorFromInvalidValue(option.Start.Line, *invalidValue) err.ShiftCharacter(option.OptionValue.Start.Character) @@ -109,6 +107,7 @@ func checkOption( } func checkMatchBlock( + d *sshconfig.SSHDocument, matchBlock *ast.SSHMatchBlock, ) []common.LSPError { errs := make([]common.LSPError, 0) @@ -117,13 +116,14 @@ func checkMatchBlock( for it.Next() { option := it.Value().(*ast.SSHOption) - errs = append(errs, checkOption(option, matchBlock)...) + errs = append(errs, checkOption(d, option, matchBlock)...) } return errs } func checkHostBlock( + d *sshconfig.SSHDocument, hostBlock *ast.SSHHostBlock, ) []common.LSPError { errs := make([]common.LSPError, 0) @@ -132,7 +132,7 @@ func checkHostBlock( for it.Next() { option := it.Value().(*ast.SSHOption) - errs = append(errs, checkOption(option, hostBlock)...) + errs = append(errs, checkOption(d, option, hostBlock)...) } return errs diff --git a/handlers/ssh_config/analyzer/quotes_test.go b/handlers/ssh_config/analyzer/quotes_test.go index 9a108d1..0b3b075 100644 --- a/handlers/ssh_config/analyzer/quotes_test.go +++ b/handlers/ssh_config/analyzer/quotes_test.go @@ -60,3 +60,36 @@ func TestIncompleteQuotesExample( t.Errorf("Expected 1 error, got %v", len(errors)) } } + +func TestDependentOptionsExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Port 1234 +CanonicalDomains example.com +`) + + option := d.FindOptionsByName("CanonicalDomains")[0] + errors := checkIsDependent(d, option.Option.Key, option.Block) + + if !(len(errors) == 1) { + t.Errorf("Expected 1 error, got %v", len(errors)) + } +} + +func TestValidDependentOptionsExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Port 1234 +CanonicalizeHostname yes +CanonicalDomains example.com +`) + + option := d.FindOptionsByName("CanonicalDomains")[0] + errors := checkIsDependent(d, option.Option.Key, option.Block) + + if len(errors) > 0 { + t.Errorf("Expected no errors, got %v", len(errors)) + } +} diff --git a/handlers/ssh_config/ast/ssh_cofig_fields_test.go b/handlers/ssh_config/ast/ssh_cofig_fields_test.go index eeba4a9..9946529 100644 --- a/handlers/ssh_config/ast/ssh_cofig_fields_test.go +++ b/handlers/ssh_config/ast/ssh_cofig_fields_test.go @@ -16,6 +16,8 @@ Host laptop Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]" HostName laptop.sdn + + `) p := NewSSHConfig() @@ -51,4 +53,9 @@ Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]" if !(thirdBlock.GetLocation().Start.Line == 5) { t.Errorf("Expected line 3, got %v", thirdBlock.GetLocation().Start.Line) } + + fourthOption, fourthBlock := p.FindOption(8) + if !(fourthOption == nil && fourthBlock == thirdBlock) { + t.Errorf("Expected no option and same block, got %v and %v", fourthOption, fourthBlock) + } } diff --git a/handlers/ssh_config/ast/ssh_config_fields.go b/handlers/ssh_config/ast/ssh_config_fields.go index 9e7d3cb..dd5c5bc 100644 --- a/handlers/ssh_config/ast/ssh_config_fields.go +++ b/handlers/ssh_config/ast/ssh_config_fields.go @@ -108,9 +108,19 @@ func (b *SSHHostBlock) GetOption() *SSHOption { return b.HostOption } +// FindBlock Gets the block based on the line number +// Note: This does not find the block strictly. +// This means an empty line will belong to the previous block +// However, this is required for example for completions, as the +// user is about to type the new option, and we therefore need to know +// which block this new option will belong to. +// +// You will probably need this in most cases func (c SSHConfig) FindBlock(line uint32) SSHBlock { it := c.Options.Iterator() - for it.Next() { + it.End() + + for it.Prev() { entry := it.Value().(SSHEntry) if entry.GetType() == SSHTypeOption { @@ -119,7 +129,7 @@ func (c SSHConfig) FindBlock(line uint32) SSHBlock { block := entry.(SSHBlock) - if block.GetLocation().Start.Line <= line && block.GetLocation().End.Line >= line { + if line >= block.GetLocation().Start.Line { return block } } diff --git a/handlers/ssh_config/document_fields.go b/handlers/ssh_config/document_fields.go index 4c6795f..8f28204 100644 --- a/handlers/ssh_config/document_fields.go +++ b/handlers/ssh_config/document_fields.go @@ -29,3 +29,10 @@ func (d SSHDocument) FindOptionsByName( return options } +func (d SSHDocument) DoesOptionExist( + name string, + block ast.SSHBlock, +) bool { + return d.FindOptionByNameAndBlock(name, block) != nil +} + diff --git a/handlers/ssh_config/fields/options.go b/handlers/ssh_config/fields/options.go index 06127d6..b52e15c 100644 --- a/handlers/ssh_config/fields/options.go +++ b/handlers/ssh_config/fields/options.go @@ -17,3 +17,5 @@ var HostDisallowedOptions = map[string]struct{}{ "EnableSSHKeysign": {}, } +var GlobalDisallowedOptions = map[string]struct{}{} + diff --git a/handlers/ssh_config/handlers/completions.go b/handlers/ssh_config/handlers/completions.go index c8cb180..7694034 100644 --- a/handlers/ssh_config/handlers/completions.go +++ b/handlers/ssh_config/handlers/completions.go @@ -22,7 +22,7 @@ func GetRootCompletions( for key, option := range fields.Options { // Check for duplicates - if d.FindOptionByNameAndBlock(key, parentBlock) != nil && !utils.KeyExists(fields.AllowedDuplicateOptions, key) { + if d.DoesOptionExist(key, parentBlock) && !utils.KeyExists(fields.AllowedDuplicateOptions, key) { continue } @@ -30,6 +30,10 @@ func GetRootCompletions( continue } + if parentBlock == nil && utils.KeyExists(fields.GlobalDisallowedOptions, key) { + continue + } + availableOptions[key] = option }