fix(server): Improve sshd_config

This commit is contained in:
Myzel394 2025-02-17 22:27:14 +01:00
parent 0c827b04cd
commit e569516aae
No known key found for this signature in database
GPG Key ID: ED20A1D1D423AF3F
7 changed files with 160 additions and 50 deletions

View File

@ -54,7 +54,6 @@ func Analyze(
}
}
analyzeValuesAreValid(ctx)
analyzeMatchBlocks(ctx)
analyzeTokens(ctx)

View File

@ -4,6 +4,7 @@ import (
"config-lsp/common"
docvalues "config-lsp/doc-values"
"config-lsp/handlers/sshd_config/ast"
"config-lsp/handlers/sshd_config/diagnostics"
"config-lsp/handlers/sshd_config/fields"
"fmt"
@ -20,7 +21,7 @@ func analyzeStructureIsValid(
switch entry.(type) {
case *ast.SSHDOption:
checkOption(ctx, entry.(*ast.SSHDOption), false)
checkOption(ctx, entry.(*ast.SSHDOption), nil)
case *ast.SSHDMatchBlock:
matchBlock := entry.(*ast.SSHDMatchBlock)
checkMatchBlock(ctx, matchBlock)
@ -31,7 +32,7 @@ func analyzeStructureIsValid(
func checkOption(
ctx *analyzerContext,
option *ast.SSHDOption,
isInMatchBlock bool,
matchBlock *ast.SSHDMatchBlock,
) {
if option.Key == nil {
return
@ -44,16 +45,19 @@ func checkOption(
docOption, found := fields.Options[key]
if !found {
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.ToLSPRange(),
Message: fmt.Sprintf("Unknown option: %s", option.Key.Key),
Severity: &common.SeverityError,
})
ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption(
option.Key.ToLSPRange(),
option.Key.Value.Value,
))
ctx.document.Indexes.UnknownOptions[option.Start.Line] = ast.SSHDOptionInfo{
Option: option,
MatchBlock: matchBlock,
}
return
}
if _, found := fields.MatchAllowedOptions[key]; !found && isInMatchBlock {
if _, found := fields.MatchAllowedOptions[key]; !found && matchBlock != nil {
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Range: option.Key.ToLSPRange(),
Message: fmt.Sprintf("Option '%s' is not allowed inside Match blocks", option.Key.Key),
@ -99,6 +103,6 @@ func checkMatchBlock(
for it.Next() {
option := it.Value().(*ast.SSHDOption)
checkOption(ctx, option, true)
checkOption(ctx, option, matchBlock)
}
}

View File

@ -18,7 +18,7 @@ ThisOptionDoesNotExist okay
diagnostics: make([]protocol.Diagnostic, 0),
}
analyzeValuesAreValid(ctx)
analyzeStructureIsValid(ctx)
if !(len(ctx.diagnostics) == 1) {
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))

View File

@ -1,39 +0,0 @@
package analyzer
import (
"config-lsp/handlers/sshd_config/diagnostics"
"config-lsp/handlers/sshd_config/fields"
)
func analyzeValuesAreValid(
ctx *analyzerContext,
) {
// Check if there are unknown options
for _, info := range ctx.document.Config.GetAllOptions() {
normalizedName := fields.CreateNormalizedName(info.Option.Key.Value.Value)
var isUnknown bool = true
// Check if the option is unknown
if info.MatchBlock == nil {
// All options are allowed
if _, found := fields.Options[normalizedName]; found {
isUnknown = false
}
} else {
// Only `MatchAllowedOptions` are allowed
if _, found := fields.MatchAllowedOptions[normalizedName]; found {
isUnknown = false
}
}
if isUnknown {
ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption(
info.Option.Key.ToLSPRange(),
info.Option.Key.Value.Value,
))
ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info
}
}
}

View File

@ -0,0 +1,20 @@
package handlers
import (
sshdconfig "config-lsp/handlers/sshd_config"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func FetchCodeActions(
d *sshdconfig.SSHDDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
if d.Indexes == nil {
return nil
}
actions := getKeywordTypoFixes(d, params)
return actions
}

View File

@ -0,0 +1,110 @@
package handlers
import (
"config-lsp/common"
sshdconfig "config-lsp/handlers/sshd_config"
"config-lsp/handlers/sshd_config/diagnostics"
"config-lsp/handlers/sshd_config/fields"
"fmt"
"github.com/hbollon/go-edlib"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func getKeywordTypoFixes(
d *sshdconfig.SSHDDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
if common.ServerOptions.NoTypoSuggestions {
return nil
}
line := params.Range.Start.Line
if typoOption, found := d.Indexes.UnknownOptions[line]; found {
name := typoOption.Option.Key.Value.Value
suggestedOptions := findSimilarOptions(name, typoOption.MatchBlock != nil)
actions := make([]protocol.CodeAction, 0, len(suggestedOptions))
kind := protocol.CodeActionKindQuickFix
for index, normalizedOptionName := range suggestedOptions {
isPreferred := index == 0
optionName := fields.FieldsNameFormattedMap[normalizedOptionName]
actions = append(actions, protocol.CodeAction{
Title: fmt.Sprintf("Typo Fix: %s", optionName),
IsPreferred: &isPreferred,
Kind: &kind,
Diagnostics: []protocol.Diagnostic{
diagnostics.GenerateUnknownOption(
typoOption.Option.Key.ToLSPRange(),
typoOption.Option.Key.Value.Value,
),
},
Edit: &protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
params.TextDocument.URI: {
{
Range: typoOption.Option.Key.ToLSPRange(),
NewText: optionName,
},
},
},
},
})
}
return actions
}
return nil
}
// Find options that are similar to the given option name.
// This is used to find typos & suggest the correct option name.
// Once an option is found that has a Damerau-Levenshtein distance of 1, it is immediately returned.
// If not, then the next 2 options of similarity 2, or 3 options of similarity 3 are returned.
// If no options with similarity <= 3 are found, then an empty slice is returned.
func findSimilarOptions(
optionName string,
restrictToMatchOptions bool,
) []fields.NormalizedOptionName {
normalizedOptionName := string(fields.CreateNormalizedName(optionName))
optionsPerSimilarity := map[uint8][]fields.NormalizedOptionName{
2: make([]fields.NormalizedOptionName, 0, 2),
3: make([]fields.NormalizedOptionName, 0, 3),
}
for name := range fields.Options {
if restrictToMatchOptions {
if _, found := fields.MatchAllowedOptions[name]; !found {
continue
}
}
normalizedName := string(name)
similarity := edlib.DamerauLevenshteinDistance(normalizedName, normalizedOptionName)
switch similarity {
case 1:
return []fields.NormalizedOptionName{name}
case 2:
optionsPerSimilarity[2] = append(optionsPerSimilarity[2], name)
if len(optionsPerSimilarity[2]) >= 2 {
return optionsPerSimilarity[2]
}
case 3:
optionsPerSimilarity[3] = append(optionsPerSimilarity[3], name)
if len(optionsPerSimilarity[3]) >= 3 {
return optionsPerSimilarity[3]
}
}
}
return append(optionsPerSimilarity[2], optionsPerSimilarity[3]...)
}

View File

@ -0,0 +1,16 @@
package lsp
import (
sshdconfig "config-lsp/handlers/sshd_config"
"config-lsp/handlers/sshd_config/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
d := sshdconfig.DocumentParserMap[params.TextDocument.URI]
actions := handlers.FetchCodeActions(d, params)
return actions, nil
}