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 { type analyzerContext struct {
document sshconfig.SSHDocument document *sshconfig.SSHDocument
diagnostics []protocol.Diagnostic diagnostics []protocol.Diagnostic
} }
func Analyze( func Analyze(
d *sshconfig.SSHDocument, d *sshconfig.SSHDocument,
) []protocol.Diagnostic { ) []protocol.Diagnostic {
errors := analyzeStructureIsValid(d) ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) > 0 { analyzeStructureIsValid(ctx)
return common.ErrsToDiagnostics(errors)
if len(ctx.diagnostics) > 0 {
return ctx.diagnostics
} }
i, indexErrors := indexes.CreateIndexes(*d.Config) i, indexErrors := indexes.CreateIndexes(*d.Config)
if len(indexErrors) > 0 {
return common.ErrsToDiagnostics(indexErrors)
}
d.Indexes = i 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) analyzeValuesAreValid(ctx)
analyzeDependents(ctx) analyzeDependents(ctx)
analyzeBlocks(ctx) analyzeBlocks(ctx)

View File

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

View File

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

View File

@ -2,100 +2,92 @@ package analyzer
import ( import (
"config-lsp/common" "config-lsp/common"
sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/handlers/ssh_config/ast" "config-lsp/handlers/ssh_config/ast"
"config-lsp/handlers/ssh_config/fields" "config-lsp/handlers/ssh_config/fields"
"config-lsp/utils" "config-lsp/utils"
"errors"
"fmt" "fmt"
protocol "github.com/tliron/glsp/protocol_3_16"
) )
func analyzeStructureIsValid( func analyzeStructureIsValid(
d *sshconfig.SSHDocument, ctx *analyzerContext,
) []common.LSPError { ) {
errs := make([]common.LSPError, 0) it := ctx.document.Config.Options.Iterator()
it := d.Config.Options.Iterator()
for it.Next() { for it.Next() {
entry := it.Value().(ast.SSHEntry) entry := it.Value().(ast.SSHEntry)
switch entry.(type) { switch entry.(type) {
case *ast.SSHOption: case *ast.SSHOption:
errs = append(errs, checkOption(d, entry.(*ast.SSHOption), nil)...) checkOption(ctx, entry.(*ast.SSHOption), nil)
case *ast.SSHMatchBlock: case *ast.SSHMatchBlock:
matchBlock := entry.(*ast.SSHMatchBlock) matchBlock := entry.(*ast.SSHMatchBlock)
errs = append(errs, checkBlock(d, matchBlock)...) checkBlock(ctx, matchBlock)
case *ast.SSHHostBlock: case *ast.SSHHostBlock:
hostBlock := entry.(*ast.SSHHostBlock) hostBlock := entry.(*ast.SSHHostBlock)
errs = append(errs, checkBlock(d, hostBlock)...) checkBlock(ctx, hostBlock)
} }
} }
return errs
} }
func checkOption( func checkOption(
d *sshconfig.SSHDocument, ctx *analyzerContext,
option *ast.SSHOption, option *ast.SSHOption,
block ast.SSHBlock, block ast.SSHBlock,
) []common.LSPError { ) {
errs := make([]common.LSPError, 0)
if option.Key == nil { if option.Key == nil {
return errs return
} }
errs = append(errs, checkIsUsingDoubleQuotes(option.Key.Value, option.Key.LocationRange)...) checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
errs = append(errs, checkQuotesAreClosed(option.Key.Value, option.Key.LocationRange)...) checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
// Check for values that are not allowed in Host blocks // Check for values that are not allowed in Host blocks
if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost { if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost {
if utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { if utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) {
errs = append(errs, common.LSPError{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange, Range: option.Key.LocationRange.ToLSPRange(),
Err: errors.New(fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key)), 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 == "" { if option.OptionValue == nil || option.OptionValue.Value.Value == "" {
errs = append(errs, common.LSPError{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange, Range: option.Key.LocationRange.ToLSPRange(),
Err: errors.New(fmt.Sprintf("Option '%s' requires a value", option.Key.Key)), Message: fmt.Sprintf("Option '%s' requires a value", option.Key.Key),
Severity: &common.SeverityError,
}) })
} else { } else {
errs = append(errs, checkIsUsingDoubleQuotes(option.OptionValue.Value, option.OptionValue.LocationRange)...) checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
errs = append(errs, checkQuotesAreClosed(option.OptionValue.Value, option.OptionValue.LocationRange)...) checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
} }
if option.Separator == nil || option.Separator.Value.Value == "" { if option.Separator == nil || option.Separator.Value.Value == "" {
errs = append(errs, common.LSPError{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.LocationRange, Range: option.Key.LocationRange.ToLSPRange(),
Err: errors.New(fmt.Sprintf("There should be a separator between an option and its value")), Message: fmt.Sprintf("There should be a separator between an option and its value"),
Severity: &common.SeverityError,
}) })
} else { } else {
errs = append(errs, checkIsUsingDoubleQuotes(option.Separator.Value, option.Separator.LocationRange)...) checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
errs = append(errs, checkQuotesAreClosed(option.Separator.Value, option.Separator.LocationRange)...) checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
} }
return errs
} }
func checkBlock( func checkBlock(
d *sshconfig.SSHDocument, ctx *analyzerContext,
block ast.SSHBlock, block ast.SSHBlock,
) []common.LSPError { ) {
errs := make([]common.LSPError, 0) checkOption(ctx, block.GetEntryOption(), block)
errs = append(errs, checkOption(d, block.GetEntryOption(), block)...)
it := block.GetOptions().Iterator() it := block.GetOptions().Iterator()
for it.Next() { for it.Next() {
option := it.Value().(*ast.SSHOption) 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 ( import (
testutils_test "config-lsp/handlers/ssh_config/test_utils" testutils_test "config-lsp/handlers/ssh_config/test_utils"
"testing" "testing"
protocol "github.com/tliron/glsp/protocol_3_16"
) )
func TestSimpleExample( func TestSimpleExample(
@ -13,11 +15,15 @@ ProxyCommand hello
User root User root
`) `)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
errors := analyzeStructureIsValid(d) analyzeStructureIsValid(ctx)
if len(errors) != 0 { if len(ctx.diagnostics) != 0 {
t.Fatalf("Expected no errors, got %v", errors) t.Fatalf("Expected no errors, got %v", ctx.diagnostics)
} }
} }
@ -29,10 +35,15 @@ ProxyCommand
User root User root
`) `)
errors := analyzeStructureIsValid(d) ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 { analyzeStructureIsValid(ctx)
t.Fatalf("Expected 1 error, got %v", errors)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
} }
} }
@ -44,10 +55,15 @@ func TestNoSeparator(
User root User root
`) `)
errors := analyzeStructureIsValid(d) ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 { analyzeStructureIsValid(ctx)
t.Fatalf("Expected 1 error, got %v", errors)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
} }
} }
@ -62,10 +78,15 @@ Host example.com
Match Match
`) `)
errors := analyzeStructureIsValid(d) ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 2 { analyzeStructureIsValid(ctx)
t.Fatalf("Expected 1 error, got %v", errors)
if len(ctx.diagnostics) != 2 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
} }
} }
@ -80,9 +101,14 @@ Host example.com
Match Match
`) `)
errors := analyzeStructureIsValid(d) ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
if len(errors) != 1 { analyzeStructureIsValid(ctx)
t.Fatalf("Expected 1 error, got %v", errors)
if len(ctx.diagnostics) != 1 {
t.Fatalf("Expected 1 error, got %v", ctx.diagnostics)
} }
} }

View File

@ -3,60 +3,52 @@ package analyzer
import ( import (
"config-lsp/common" "config-lsp/common"
commonparser "config-lsp/common/parser" commonparser "config-lsp/common/parser"
sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/utils" "config-lsp/utils"
"errors"
"strings" "strings"
protocol "github.com/tliron/glsp/protocol_3_16"
) )
func analyzeQuotesAreValid( func analyzeQuotesAreValid(
d *sshconfig.SSHDocument, ctx *analyzerContext,
) []common.LSPError { ) {
errs := make([]common.LSPError, 0) 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() { checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
errs = append(errs, checkIsUsingDoubleQuotes(info.Option.Key.Value, info.Option.Key.LocationRange)...) checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.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)...)
} }
return errs
} }
func checkIsUsingDoubleQuotes( func checkIsUsingDoubleQuotes(
ctx *analyzerContext,
value commonparser.ParsedString, value commonparser.ParsedString,
valueRange common.LocationRange, valueRange common.LocationRange,
) []common.LSPError { ) {
quoteRanges := utils.GetQuoteRanges(value.Raw) quoteRanges := utils.GetQuoteRanges(value.Raw)
singleQuotePosition := strings.Index(value.Raw, "'") singleQuotePosition := strings.Index(value.Raw, "'")
// Single quoe // Single quoe
if singleQuotePosition != -1 && !quoteRanges.IsCharInside(singleQuotePosition) { if singleQuotePosition != -1 && !quoteRanges.IsCharInside(singleQuotePosition) {
return []common.LSPError{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
{ Range: valueRange.ToLSPRange(),
Range: valueRange, Message: "ssh_config does not support single quotes. Use double quotes (\") instead.",
Err: errors.New("ssh_config does not support single quotes. Use double quotes (\") instead."), Severity: &common.SeverityError,
}, })
}
} }
return nil
} }
func checkQuotesAreClosed( func checkQuotesAreClosed(
ctx *analyzerContext,
value commonparser.ParsedString, value commonparser.ParsedString,
valueRange common.LocationRange, valueRange common.LocationRange,
) []common.LSPError { ) {
if strings.Count(value.Raw, "\"")%2 != 0 { if strings.Count(value.Raw, "\"")%2 != 0 {
return []common.LSPError{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
{ Range: valueRange.ToLSPRange(),
Range: valueRange, Message: "There are unclosed quotes here. Make sure all quotes are closed.",
Err: errors.New("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, ` d := testutils_test.DocumentFromInput(t, `
PermitRootLogin 'yes' PermitRootLogin 'yes'
`) `)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d) if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
} }
} }
@ -27,11 +30,14 @@ func TestSingleQuotesKeyAndOptionExample(
d := testutils_test.DocumentFromInput(t, ` d := testutils_test.DocumentFromInput(t, `
'Port' '22' 'Port' '22'
`) `)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d) if !(len(ctx.diagnostics) == 2) {
t.Errorf("Expected 2 ctx.diagnostics, got %v", len(ctx.diagnostics))
if !(len(errors) == 2) {
t.Errorf("Expected 2 errors, got %v", len(errors))
} }
} }
@ -41,11 +47,14 @@ func TestSimpleUnclosedQuoteExample(
d := testutils_test.DocumentFromInput(t, ` d := testutils_test.DocumentFromInput(t, `
PermitRootLogin "yes PermitRootLogin "yes
`) `)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d) if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
} }
} }
@ -55,11 +64,14 @@ func TestIncompleteQuotesExample(
d := testutils_test.DocumentFromInput(t, ` d := testutils_test.DocumentFromInput(t, `
"Port "Port
`) `)
ctx := &analyzerContext{
document: d,
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeQuotesAreValid(ctx)
errors := analyzeQuotesAreValid(d) if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", len(errors))
} }
} }
@ -71,7 +83,7 @@ Port 1234
CanonicalDomains example.com CanonicalDomains example.com
`) `)
ctx := &analyzerContext{ ctx := &analyzerContext{
document: *d, document: d,
diagnostics: make([]protocol.Diagnostic, 0), diagnostics: make([]protocol.Diagnostic, 0),
} }
@ -92,7 +104,7 @@ CanonicalizeHostname yes
CanonicalDomains example.com CanonicalDomains example.com
`) `)
ctx := &analyzerContext{ ctx := &analyzerContext{
document: *d, document: d,
diagnostics: make([]protocol.Diagnostic, 0), diagnostics: make([]protocol.Diagnostic, 0),
} }

View File

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