feat(ssh_config): Add analyzer; Overall improvements

This commit is contained in:
Myzel394 2024-09-28 11:35:15 +02:00
parent 82301cf0cb
commit e19ad01fd8
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
10 changed files with 163 additions and 13 deletions

View File

@ -1,6 +1,8 @@
package common package common
import ( import (
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
@ -30,3 +32,12 @@ type SyntaxError struct {
func (s SyntaxError) Error() string { func (s SyntaxError) Error() string {
return s.Message return s.Message
} }
func ErrsToDiagnostics(errs []LSPError) []protocol.Diagnostic {
return utils.Map(
errs,
func(err LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
}

View File

@ -1,12 +1,33 @@
package analyzer package analyzer
import ( import (
"config-lsp/common"
sshconfig "config-lsp/handlers/ssh_config" sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/handlers/ssh_config/indexes"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
func Analyze( func Analyze(
d *sshconfig.SSHDocument, d *sshconfig.SSHDocument,
) []protocol.Diagnostic { ) []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)
} }

View File

@ -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
}

View File

@ -22,13 +22,13 @@ func analyzeStructureIsValid(
switch entry.(type) { switch entry.(type) {
case *ast.SSHOption: case *ast.SSHOption:
errs = append(errs, checkOption(entry.(*ast.SSHOption), nil)...) errs = append(errs, checkOption(d, entry.(*ast.SSHOption), nil)...)
case *ast.SSHMatchBlock: case *ast.SSHMatchBlock:
matchBlock := entry.(*ast.SSHMatchBlock) matchBlock := entry.(*ast.SSHMatchBlock)
errs = append(errs, checkMatchBlock(matchBlock)...) errs = append(errs, checkMatchBlock(d, matchBlock)...)
case *ast.SSHHostBlock: case *ast.SSHHostBlock:
hostBlock := entry.(*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( func checkOption(
d *sshconfig.SSHDocument,
option *ast.SSHOption, option *ast.SSHOption,
block ast.SSHBlock, block ast.SSHBlock,
) []common.LSPError { ) []common.LSPError {
@ -78,13 +79,10 @@ func checkOption(
} else { } else {
errs = append(errs, checkIsUsingDoubleQuotes(option.OptionValue.Value, option.OptionValue.LocationRange)...) errs = append(errs, checkIsUsingDoubleQuotes(option.OptionValue.Value, option.OptionValue.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(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 = append(
errs, errs,
utils.Map( utils.Map(
invalidValues, docOption.CheckIsValid(option.OptionValue.Value.Value),
func(invalidValue *docvalues.InvalidValue) common.LSPError { func(invalidValue *docvalues.InvalidValue) common.LSPError {
err := docvalues.LSPErrorFromInvalidValue(option.Start.Line, *invalidValue) err := docvalues.LSPErrorFromInvalidValue(option.Start.Line, *invalidValue)
err.ShiftCharacter(option.OptionValue.Start.Character) err.ShiftCharacter(option.OptionValue.Start.Character)
@ -109,6 +107,7 @@ func checkOption(
} }
func checkMatchBlock( func checkMatchBlock(
d *sshconfig.SSHDocument,
matchBlock *ast.SSHMatchBlock, matchBlock *ast.SSHMatchBlock,
) []common.LSPError { ) []common.LSPError {
errs := make([]common.LSPError, 0) errs := make([]common.LSPError, 0)
@ -117,13 +116,14 @@ func checkMatchBlock(
for it.Next() { for it.Next() {
option := it.Value().(*ast.SSHOption) option := it.Value().(*ast.SSHOption)
errs = append(errs, checkOption(option, matchBlock)...) errs = append(errs, checkOption(d, option, matchBlock)...)
} }
return errs return errs
} }
func checkHostBlock( func checkHostBlock(
d *sshconfig.SSHDocument,
hostBlock *ast.SSHHostBlock, hostBlock *ast.SSHHostBlock,
) []common.LSPError { ) []common.LSPError {
errs := make([]common.LSPError, 0) errs := make([]common.LSPError, 0)
@ -132,7 +132,7 @@ func checkHostBlock(
for it.Next() { for it.Next() {
option := it.Value().(*ast.SSHOption) option := it.Value().(*ast.SSHOption)
errs = append(errs, checkOption(option, hostBlock)...) errs = append(errs, checkOption(d, option, hostBlock)...)
} }
return errs return errs

View File

@ -60,3 +60,36 @@ func TestIncompleteQuotesExample(
t.Errorf("Expected 1 error, got %v", len(errors)) 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))
}
}

View File

@ -16,6 +16,8 @@ Host laptop
Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]" Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]"
HostName laptop.sdn HostName laptop.sdn
`) `)
p := NewSSHConfig() p := NewSSHConfig()
@ -51,4 +53,9 @@ Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]"
if !(thirdBlock.GetLocation().Start.Line == 5) { if !(thirdBlock.GetLocation().Start.Line == 5) {
t.Errorf("Expected line 3, got %v", thirdBlock.GetLocation().Start.Line) 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)
}
} }

View File

@ -108,9 +108,19 @@ func (b *SSHHostBlock) GetOption() *SSHOption {
return b.HostOption 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 { func (c SSHConfig) FindBlock(line uint32) SSHBlock {
it := c.Options.Iterator() it := c.Options.Iterator()
for it.Next() { it.End()
for it.Prev() {
entry := it.Value().(SSHEntry) entry := it.Value().(SSHEntry)
if entry.GetType() == SSHTypeOption { if entry.GetType() == SSHTypeOption {
@ -119,7 +129,7 @@ func (c SSHConfig) FindBlock(line uint32) SSHBlock {
block := entry.(SSHBlock) block := entry.(SSHBlock)
if block.GetLocation().Start.Line <= line && block.GetLocation().End.Line >= line { if line >= block.GetLocation().Start.Line {
return block return block
} }
} }

View File

@ -29,3 +29,10 @@ func (d SSHDocument) FindOptionsByName(
return options return options
} }
func (d SSHDocument) DoesOptionExist(
name string,
block ast.SSHBlock,
) bool {
return d.FindOptionByNameAndBlock(name, block) != nil
}

View File

@ -17,3 +17,5 @@ var HostDisallowedOptions = map[string]struct{}{
"EnableSSHKeysign": {}, "EnableSSHKeysign": {},
} }
var GlobalDisallowedOptions = map[string]struct{}{}

View File

@ -22,7 +22,7 @@ func GetRootCompletions(
for key, option := range fields.Options { for key, option := range fields.Options {
// Check for duplicates // 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 continue
} }
@ -30,6 +30,10 @@ func GetRootCompletions(
continue continue
} }
if parentBlock == nil && utils.KeyExists(fields.GlobalDisallowedOptions, key) {
continue
}
availableOptions[key] = option availableOptions[key] = option
} }