feat(ssh_config): Improve context

This commit is contained in:
Myzel394 2024-10-01 21:44:37 +02:00
parent 3d389eb53f
commit 1e6054ccc1
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
8 changed files with 145 additions and 125 deletions

View File

@ -9,34 +9,32 @@ import (
)
type analyzerContext struct {
document sshconfig.SSHDocument
document *sshconfig.SSHDocument
diagnostics []protocol.Diagnostic
}
func Analyze(
d *sshconfig.SSHDocument,
) []protocol.Diagnostic {
errors := analyzeStructureIsValid(d)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) > 0 {
return common.ErrsToDiagnostics(errors)
analyzeStructureIsValid(ctx)
if len(ctx.diagnostics) > 0 {
return ctx.diagnostics
}
i, indexErrors := indexes.CreateIndexes(*d.Config)
if len(indexErrors) > 0 {
return common.ErrsToDiagnostics(indexErrors)
}
d.Indexes = i
errors = append(errors, indexErrors...)
if len(errors) > 0 {
return common.ErrsToDiagnostics(errors)
}
ctx := &analyzerContext{
document: *d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeValuesAreValid(ctx)
analyzeDependents(ctx)
analyzeBlocks(ctx)

View File

@ -14,7 +14,7 @@ func TestBlockEmptyBlock(
Host *
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}

View File

@ -14,7 +14,7 @@ func TestMatchInvalidAllArgument(
Match user lena all
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}

View File

@ -2,100 +2,92 @@ package analyzer
import (
"config-lsp/common"
sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/handlers/ssh_config/ast"
"config-lsp/handlers/ssh_config/fields"
"config-lsp/utils"
"errors"
"fmt"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func analyzeStructureIsValid(
d *sshconfig.SSHDocument,
) []common.LSPError {
errs := make([]common.LSPError, 0)
it := d.Config.Options.Iterator()
ctx *analyzerContext,
) {
it := ctx.document.Config.Options.Iterator()
for it.Next() {
entry := it.Value().(ast.SSHEntry)
switch entry.(type) {
case *ast.SSHOption:
errs = append(errs, checkOption(d, entry.(*ast.SSHOption), nil)...)
checkOption(ctx, entry.(*ast.SSHOption), nil)
case *ast.SSHMatchBlock:
matchBlock := entry.(*ast.SSHMatchBlock)
errs = append(errs, checkBlock(d, matchBlock)...)
checkBlock(ctx, matchBlock)
case *ast.SSHHostBlock:
hostBlock := entry.(*ast.SSHHostBlock)
errs = append(errs, checkBlock(d, hostBlock)...)
checkBlock(ctx, hostBlock)
}
}
return errs
}
func checkOption(
d *sshconfig.SSHDocument,
ctx *analyzerContext,
option *ast.SSHOption,
block ast.SSHBlock,
) []common.LSPError {
errs := make([]common.LSPError, 0)
) {
if option.Key == nil {
return errs
return
}
errs = append(errs, checkIsUsingDoubleQuotes(option.Key.Value, option.Key.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(option.Key.Value, option.Key.LocationRange)...)
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
// Check for values that are not allowed in Host blocks
if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost {
if utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) {
errs = append(errs, common.LSPError{
Range: option.Key.LocationRange,
Err: errors.New(fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key)),
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange.ToLSPRange(),
Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key),
Severity: &common.SeverityError,
})
}
}
if option.OptionValue == nil || option.OptionValue.Value.Value == "" {
errs = append(errs, common.LSPError{
Range: option.Key.LocationRange,
Err: errors.New(fmt.Sprintf("Option '%s' requires a value", option.Key.Key)),
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange.ToLSPRange(),
Message: fmt.Sprintf("Option '%s' requires a value", option.Key.Key),
Severity: &common.SeverityError,
})
} else {
errs = append(errs, checkIsUsingDoubleQuotes(option.OptionValue.Value, option.OptionValue.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(option.OptionValue.Value, option.OptionValue.LocationRange)...)
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
}
if option.Separator == nil || option.Separator.Value.Value == "" {
errs = append(errs, common.LSPError{
Range: option.Key.LocationRange,
Err: errors.New(fmt.Sprintf("There should be a separator between an option and its value")),
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange.ToLSPRange(),
Message: fmt.Sprintf("There should be a separator between an option and its value"),
Severity: &common.SeverityError,
})
} else {
errs = append(errs, checkIsUsingDoubleQuotes(option.Separator.Value, option.Separator.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(option.Separator.Value, option.Separator.LocationRange)...)
checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
}
return errs
}
func checkBlock(
d *sshconfig.SSHDocument,
ctx *analyzerContext,
block ast.SSHBlock,
) []common.LSPError {
errs := make([]common.LSPError, 0)
errs = append(errs, checkOption(d, block.GetEntryOption(), block)...)
) {
checkOption(ctx, block.GetEntryOption(), block)
it := block.GetOptions().Iterator()
for it.Next() {
option := it.Value().(*ast.SSHOption)
errs = append(errs, checkOption(d, option, block)...)
checkOption(ctx, option, block)
}
return errs
}

View File

@ -3,6 +3,8 @@ package analyzer
import (
testutils_test "config-lsp/handlers/ssh_config/test_utils"
"testing"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TestSimpleExample(
@ -13,11 +15,15 @@ ProxyCommand hello
User root
`)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
errors := analyzeStructureIsValid(d)
analyzeStructureIsValid(ctx)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
if len(ctx.diagnostics) != 0 {
t.Fatalf("Expected no errors, got %v", ctx.diagnostics)
}
}
@ -29,10 +35,15 @@ ProxyCommand
User root
`)
errors := analyzeStructureIsValid(d)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 {
t.Fatalf("Expected 1 error, got %v", errors)
analyzeStructureIsValid(ctx)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
}
}
@ -44,10 +55,15 @@ func TestNoSeparator(
User root
`)
errors := analyzeStructureIsValid(d)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 {
t.Fatalf("Expected 1 error, got %v", errors)
analyzeStructureIsValid(ctx)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
}
}
@ -62,10 +78,15 @@ Host example.com
Match
`)
errors := analyzeStructureIsValid(d)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 2 {
t.Fatalf("Expected 1 error, got %v", errors)
analyzeStructureIsValid(ctx)
if len(ctx.diagnostics) != 2 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
}
}
@ -80,9 +101,14 @@ Host example.com
Match
`)
errors := analyzeStructureIsValid(d)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 {
t.Fatalf("Expected 1 error, got %v", errors)
analyzeStructureIsValid(ctx)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
}
}

View File

@ -3,60 +3,52 @@ package analyzer
import (
"config-lsp/common"
commonparser "config-lsp/common/parser"
sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/utils"
"errors"
"strings"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func analyzeQuotesAreValid(
d *sshconfig.SSHDocument,
) []common.LSPError {
errs := make([]common.LSPError, 0)
ctx *analyzerContext,
) {
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)
for _, info := range d.Config.GetAllOptions() {
errs = append(errs, checkIsUsingDoubleQuotes(info.Option.Key.Value, info.Option.Key.LocationRange)...)
errs = append(errs, checkIsUsingDoubleQuotes(info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(info.Option.Key.Value, info.Option.Key.LocationRange)...)
errs = append(errs, checkQuotesAreClosed(info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)...)
checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
}
return errs
}
func checkIsUsingDoubleQuotes(
ctx *analyzerContext,
value commonparser.ParsedString,
valueRange common.LocationRange,
) []common.LSPError {
) {
quoteRanges := utils.GetQuoteRanges(value.Raw)
singleQuotePosition := strings.Index(value.Raw, "'")
// Single quoe
if singleQuotePosition != -1 && !quoteRanges.IsCharInside(singleQuotePosition) {
return []common.LSPError{
{
Range: valueRange,
Err: errors.New("ssh_config does not support single quotes. Use double quotes (\") instead."),
},
}
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: valueRange.ToLSPRange(),
Message: "ssh_config does not support single quotes. Use double quotes (\") instead.",
Severity: &common.SeverityError,
})
}
return nil
}
func checkQuotesAreClosed(
ctx *analyzerContext,
value commonparser.ParsedString,
valueRange common.LocationRange,
) []common.LSPError {
) {
if strings.Count(value.Raw, "\"")%2 != 0 {
return []common.LSPError{
{
Range: valueRange,
Err: errors.New("There are unclosed quotes here. Make sure all quotes are closed."),
},
}
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: valueRange.ToLSPRange(),
Message: "There are unclosed quotes here. Make sure all quotes are closed.",
Severity: &common.SeverityError,
})
}
return nil
}

View File

@ -13,11 +13,14 @@ func TestSimpleInvalidQuotesExample(
d := testutils_test.DocumentFromInput(t, `
PermitRootLogin 'yes'
`)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
}
}
@ -27,11 +30,14 @@ func TestSingleQuotesKeyAndOptionExample(
d := testutils_test.DocumentFromInput(t, `
'Port' '22'
`)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d)
if !(len(errors) == 2) {
t.Errorf("Expected 2 errors, got %v", len(errors))
if !(len(ctx.diagnostics) == 2) {
t.Errorf("Expected 2 ctx.diagnostics, got %v", len(ctx.diagnostics))
}
}
@ -41,11 +47,14 @@ func TestSimpleUnclosedQuoteExample(
d := testutils_test.DocumentFromInput(t, `
PermitRootLogin "yes
`)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
}
}
@ -55,11 +64,14 @@ func TestIncompleteQuotesExample(
d := testutils_test.DocumentFromInput(t, `
"Port
`)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
}
}
@ -71,7 +83,7 @@ Port 1234
CanonicalDomains example.com
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
@ -92,7 +104,7 @@ CanonicalizeHostname yes
CanonicalDomains example.com
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}

View File

@ -14,7 +14,7 @@ func TestUnknownOptionExample(
ThisOptionDoesNotExist okay
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
@ -33,7 +33,7 @@ IgnoreUnknown ThisOptionDoesNotExist
ThisOptionDoesNotExist okay
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
@ -52,7 +52,7 @@ ThisOptionDoesNotExist okay
IgnoreUnknown ThisOptionDoesNotExist
`)
ctx := &analyzerContext{
document: *d,
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}