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
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()
},
)
}

View File

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

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) {
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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