mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-18 23:15:26 +02:00
feat: Add diagnostics; Improvements
This commit is contained in:
parent
b9e8fc6e55
commit
0ae6c8d9e2
@ -2,13 +2,15 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type Value interface {
|
||||
getTypeDescription() []string
|
||||
GetTypeDescription() []string
|
||||
CheckIsValid(value string) error
|
||||
}
|
||||
|
||||
type EnumValue struct {
|
||||
@ -19,7 +21,7 @@ type EnumValue struct {
|
||||
EnforceValues bool
|
||||
}
|
||||
|
||||
func (v EnumValue) getTypeDescription() []string {
|
||||
func (v EnumValue) GetTypeDescription() []string {
|
||||
lines := make([]string, len(v.Values)+1)
|
||||
lines[0] = "Enum of:"
|
||||
|
||||
@ -29,12 +31,42 @@ func (v EnumValue) getTypeDescription() []string {
|
||||
|
||||
return lines
|
||||
}
|
||||
func (v EnumValue) CheckIsValid(value string) error {
|
||||
if !v.EnforceValues {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, validValue := range v.Values {
|
||||
if validValue == value {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ValueNotInEnumError{
|
||||
ProvidedValue: value,
|
||||
AvailableValues: v.Values,
|
||||
}
|
||||
}
|
||||
|
||||
type PositiveNumberValue struct{}
|
||||
|
||||
func (v PositiveNumberValue) getTypeDescription() []string {
|
||||
func (v PositiveNumberValue) GetTypeDescription() []string {
|
||||
return []string{"Positive number"}
|
||||
}
|
||||
func (v PositiveNumberValue) CheckIsValid(value string) error {
|
||||
number, err := strconv.Atoi(value)
|
||||
|
||||
if err != nil {
|
||||
return NotANumberError{}
|
||||
}
|
||||
|
||||
if number < 0 {
|
||||
return NumberIsNotPositiveError{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ArrayValue struct {
|
||||
SubValue Value
|
||||
@ -42,25 +74,57 @@ type ArrayValue struct {
|
||||
AllowDuplicates bool
|
||||
}
|
||||
|
||||
func (v ArrayValue) getTypeDescription() []string {
|
||||
func (v ArrayValue) GetTypeDescription() []string {
|
||||
subValue := v.SubValue.(Value)
|
||||
|
||||
return append(
|
||||
[]string{"An Array separated by " + v.Separator + " of:"},
|
||||
subValue.getTypeDescription()...,
|
||||
subValue.GetTypeDescription()...,
|
||||
)
|
||||
}
|
||||
func (v ArrayValue) CheckIsValid(value string) error {
|
||||
values := strings.Split(value, v.Separator)
|
||||
|
||||
for _, subValue := range values {
|
||||
err := v.SubValue.CheckIsValid(subValue)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !v.AllowDuplicates {
|
||||
valuesOccurrences := SliceToMap(values, 0)
|
||||
|
||||
// Only continue if there are actually duplicate values
|
||||
if len(values) != len(valuesOccurrences) {
|
||||
for _, subValue := range values {
|
||||
valuesOccurrences[subValue]++
|
||||
}
|
||||
|
||||
duplicateValues := FilterMapWhere(valuesOccurrences, func(_ string, value int) bool {
|
||||
return value > 1
|
||||
})
|
||||
|
||||
return ArrayContainsDuplicatesError{
|
||||
Duplicates: KeysOfMap(duplicateValues),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OrValue struct {
|
||||
Values []Value
|
||||
}
|
||||
|
||||
func (v OrValue) getTypeDescription() []string {
|
||||
func (v OrValue) GetTypeDescription() []string {
|
||||
lines := make([]string, 0)
|
||||
|
||||
for _, subValueRaw := range v.Values {
|
||||
subValue := subValueRaw.(Value)
|
||||
subLines := subValue.getTypeDescription()
|
||||
subLines := subValue.GetTypeDescription()
|
||||
|
||||
for index, line := range subLines {
|
||||
if strings.HasPrefix(line, "\t*") {
|
||||
@ -78,21 +142,48 @@ func (v OrValue) getTypeDescription() []string {
|
||||
lines...,
|
||||
)
|
||||
}
|
||||
func (v OrValue) CheckIsValid(value string) error {
|
||||
var firstError error = nil
|
||||
|
||||
for _, subValue := range v.Values {
|
||||
err := subValue.CheckIsValid(value)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
}
|
||||
|
||||
return firstError
|
||||
}
|
||||
|
||||
type StringValue struct{}
|
||||
|
||||
func (v StringValue) getTypeDescription() []string {
|
||||
func (v StringValue) GetTypeDescription() []string {
|
||||
return []string{"String"}
|
||||
}
|
||||
|
||||
func (v StringValue) CheckIsValid(value string) error {
|
||||
if value == "" {
|
||||
return EmptyStringError{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CustomValue struct {
|
||||
FetchValue func() Value
|
||||
}
|
||||
|
||||
func (v CustomValue) getTypeDescription() []string {
|
||||
func (v CustomValue) GetTypeDescription() []string {
|
||||
return []string{"Custom"}
|
||||
}
|
||||
|
||||
func (v CustomValue) CheckIsValid(value string) error {
|
||||
return v.FetchValue().CheckIsValid(value)
|
||||
}
|
||||
|
||||
type Prefix struct {
|
||||
Prefix string
|
||||
Meaning string
|
||||
@ -102,8 +193,8 @@ type PrefixWithMeaningValue struct {
|
||||
SubValue Value
|
||||
}
|
||||
|
||||
func (v PrefixWithMeaningValue) getTypeDescription() []string {
|
||||
subDescription := v.SubValue.getTypeDescription()
|
||||
func (v PrefixWithMeaningValue) GetTypeDescription() []string {
|
||||
subDescription := v.SubValue.GetTypeDescription()
|
||||
|
||||
prefixDescription := Map(v.Prefixes, func(prefix Prefix) string {
|
||||
return fmt.Sprintf("_%s_ -> %s", prefix.Prefix, prefix.Meaning)
|
||||
@ -117,6 +208,10 @@ func (v PrefixWithMeaningValue) getTypeDescription() []string {
|
||||
)
|
||||
}
|
||||
|
||||
func (v PrefixWithMeaningValue) CheckIsValid(value string) error {
|
||||
return v.SubValue.CheckIsValid(value)
|
||||
}
|
||||
|
||||
type PathType uint8
|
||||
|
||||
const (
|
||||
@ -129,13 +224,13 @@ type PathValue struct {
|
||||
RequiredType PathType
|
||||
}
|
||||
|
||||
func (v PathValue) getTypeDescription() []string {
|
||||
func (v PathValue) GetTypeDescription() []string {
|
||||
hints := make([]string, 0)
|
||||
|
||||
switch v.RequiredType {
|
||||
case PathTypeExistenceOptional:
|
||||
hints = append(hints, "Optional")
|
||||
break;
|
||||
break
|
||||
case PathTypeFile:
|
||||
hints = append(hints, "File")
|
||||
case PathTypeDirectory:
|
||||
@ -145,13 +240,35 @@ func (v PathValue) getTypeDescription() []string {
|
||||
return []string{strings.Join(hints, ", ")}
|
||||
}
|
||||
|
||||
func (v PathValue) CheckIsValid(value string) error {
|
||||
if !DoesPathExist(value) {
|
||||
return PathDoesNotExistError{}
|
||||
}
|
||||
|
||||
isValid := false
|
||||
|
||||
if (v.RequiredType & PathTypeFile) == PathTypeFile {
|
||||
isValid = isValid && IsPathFile(value)
|
||||
}
|
||||
|
||||
if (v.RequiredType & PathTypeDirectory) == PathTypeDirectory {
|
||||
isValid = isValid && IsPathDirectory(value)
|
||||
}
|
||||
|
||||
if isValid {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PathInvalidError{}
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Documentation string
|
||||
Value Value
|
||||
}
|
||||
|
||||
func GetDocumentation(o *Option) protocol.MarkupContent {
|
||||
typeDescription := strings.Join(o.Value.getTypeDescription(), "\n")
|
||||
typeDescription := strings.Join(o.Value.GetTypeDescription(), "\n")
|
||||
|
||||
return protocol.MarkupContent{
|
||||
Kind: protocol.MarkupKindPlainText,
|
||||
|
@ -39,10 +39,55 @@ func (e LineNotFoundError) Error() string {
|
||||
}
|
||||
|
||||
type ValueNotInEnumError struct {
|
||||
availableValues []string
|
||||
providedValue string
|
||||
AvailableValues []string
|
||||
ProvidedValue string
|
||||
}
|
||||
|
||||
type NotANumberError struct{}
|
||||
|
||||
func (e NotANumberError) Error() string {
|
||||
return "This is not a number"
|
||||
}
|
||||
|
||||
type NumberIsNotPositiveError struct{}
|
||||
|
||||
func (e NumberIsNotPositiveError) Error() string {
|
||||
return "This number is not positive"
|
||||
}
|
||||
|
||||
type EmptyStringError struct{}
|
||||
|
||||
func (e EmptyStringError) Error() string {
|
||||
return "This setting may not be empty"
|
||||
}
|
||||
|
||||
type ArrayContainsDuplicatesError struct {
|
||||
Duplicates []string
|
||||
}
|
||||
|
||||
func (e ArrayContainsDuplicatesError) Error() string {
|
||||
return fmt.Sprintf("Array contains the following duplicate values: %s", strings.Join(e.Duplicates, ","))
|
||||
}
|
||||
|
||||
type PathDoesNotExistError struct{}
|
||||
|
||||
func (e PathDoesNotExistError) Error() string {
|
||||
return "This path does not exist"
|
||||
}
|
||||
|
||||
type PathInvalidError struct{}
|
||||
|
||||
func (e PathInvalidError) Error() string {
|
||||
return "This path is invalid"
|
||||
}
|
||||
|
||||
type ValueError struct {
|
||||
Line int
|
||||
Start int
|
||||
End int
|
||||
Error error
|
||||
}
|
||||
|
||||
func (e ValueNotInEnumError) Error() string {
|
||||
return fmt.Sprint("'%s' is not valid. Select one from: %s", e.providedValue, strings.Join(e.availableValues, ","))
|
||||
return fmt.Sprintf("'%s' is not valid. Select one from: %s", e.ProvidedValue, strings.Join(e.AvailableValues, ","))
|
||||
}
|
||||
|
@ -28,3 +28,75 @@ func Map[T any, O any](values []T, f func(T) O) []O {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func SliceToSet[T comparable](values []T) map[T]struct{} {
|
||||
set := make(map[T]struct{})
|
||||
|
||||
for _, value := range values {
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func SliceToMap[T comparable, O any](values []T, defaultValue O) map[T]O {
|
||||
set := make(map[T]O)
|
||||
|
||||
for _, value := range values {
|
||||
set[value] = defaultValue
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func FilterWhere[T any](values []T, f func(T) bool) []T {
|
||||
result := make([]T, 0)
|
||||
|
||||
for _, value := range values {
|
||||
if f(value) {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func FilterMapWhere[T comparable, O any](values map[T]O, f func(T, O) bool) map[T]O {
|
||||
result := make(map[T]O)
|
||||
|
||||
for key, value := range values {
|
||||
if f(key, value) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func KeysOfMap[T comparable, O any](values map[T]O) []T {
|
||||
keys := make([]T, 0)
|
||||
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func DoesPathExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func IsPathDirectory(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func IsPathFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
29
handlers/openssh/analyzer.go
Normal file
29
handlers/openssh/analyzer.go
Normal file
@ -0,0 +1,29 @@
|
||||
package handlers
|
||||
|
||||
import "config-lsp/common"
|
||||
|
||||
// TODO: Cache options in a map like: EnumValues -> []Option
|
||||
// for faster lookup
|
||||
|
||||
func AnalyzeValue() []common.ValueError {
|
||||
errors := make([]common.ValueError, 0)
|
||||
|
||||
for optionName, line := range Parser.Lines {
|
||||
documentationOption := Options[optionName]
|
||||
|
||||
err := documentationOption.Value.CheckIsValid(line.Value)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, common.ValueError{
|
||||
Line: line.Position.Line,
|
||||
Start: len(optionName) + len(" "),
|
||||
End: len(optionName) + len(" ") + len(line.Value),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// func AnalyzeSSHConfigIssues() []common.ParserError {}
|
@ -54,3 +54,31 @@ func SendDiagnosticsFromParserErrors(context *glsp.Context, uri protocol.Documen
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func SendDiagnosticsFromAnalyzerErrors(context *glsp.Context, uri protocol.DocumentUri, errors []common.ValueError) {
|
||||
diagnosticErrors := make([]protocol.Diagnostic, 0)
|
||||
|
||||
for _, err := range errors {
|
||||
diagnosticErrors = append(diagnosticErrors, protocol.Diagnostic{
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: uint32(err.Line),
|
||||
Character: uint32(err.Start),
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: uint32(err.Line),
|
||||
Character: uint32(err.End),
|
||||
},
|
||||
},
|
||||
Message: err.Error.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
context.Notify(
|
||||
"textDocument/publishDiagnostics",
|
||||
protocol.PublishDiagnosticsParams{
|
||||
URI: uri,
|
||||
Diagnostics: diagnosticErrors,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -39,4 +39,3 @@ func QueryOpenSSHOptions(
|
||||
|
||||
return availableQueries, nil
|
||||
}
|
||||
|
||||
|
@ -269,7 +269,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
"DenyUsers": common.NewOption(`This keyword can be followed by a list of user name patterns, separated by spaces. Login is disallowed for user names that match one of the patterns. Only user names are valid; a numerical user ID is not recognized. By default, login is allowed for all users. If the pattern takes the form USER@HOST then USER and HOST are separately checked, restricting logins to particular users from particular hosts. HOST criteria may additionally contain addresses to match in CIDR address/masklen format. The allow/deny users directives are processed in the following order: DenyUsers, AllowUsers.
|
||||
See PATTERNS in ssh_config(5) for more information on patterns. This keyword may appear multiple times in sshd_config with each instance appending to the list.`,
|
||||
common.UserValue(" ", false),
|
||||
),
|
||||
),
|
||||
"DisableForwarding": common.NewOption(
|
||||
`Disables all forwarding features, including X11, ssh-agent(1), TCP and StreamLocal. This option overrides all other forwarding-related options and may simplify restricted configurations.`,
|
||||
BooleanEnumValue,
|
||||
@ -320,7 +320,6 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
options, _ = QueryOpenSSHOptions("HostbasedAcceptedKeyTypes")
|
||||
}
|
||||
|
||||
|
||||
return PrefixPlusMinusCaret(options)
|
||||
},
|
||||
},
|
||||
@ -437,8 +436,8 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
"sntrup761x25519-sha512@openssh.com",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
// "ListenAddress": `Specifies the local addresses sshd(8) should listen on. The following forms may be used:
|
||||
// ListenAddress hostname|address [rdomain domain] ListenAddress hostname:port [rdomain domain] ListenAddress IPv4_address:port [rdomain domain] ListenAddress [hostname|address]:port [rdomain domain]
|
||||
// The optional rdomain qualifier requests sshd(8) listen in an explicit routing domain. If port is not specified, sshd will listen on the address and all Port options specified. The default is to listen on all local addresses on the current default routing domain. Multiple ListenAddress options are permitted. For more information on routing domains, see rdomain(4).`,
|
||||
@ -509,7 +508,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
common.PathValue{
|
||||
RequiredType: common.PathTypeFile,
|
||||
},
|
||||
),
|
||||
),
|
||||
"PasswordAuthentication": common.NewOption(`Specifies whether password authentication is allowed. The default is yes.`,
|
||||
BooleanEnumValue,
|
||||
),
|
||||
@ -603,7 +602,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
return PrefixPlusMinusCaret(options)
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
"PubkeyAuthOptions": common.NewOption(`Sets one or more public key authentication options. The supported keywords are: none (the default; indicating no additional options are enabled), touch-required and verify-required.
|
||||
The touch-required option causes public key authentication using a FIDO authenticator algorithm (i.e. ecdsa-sk or ed25519-sk) to always require the signature to attest that a physically present user explicitly confirmed the authentication (usually by touching the authenticator). By default, sshd(8) requires user presence unless overridden with an authorized_keys option. The touch-required flag disables this override.
|
||||
The verify-required option requires a FIDO key signature attest that the user was verified, e.g. via a PIN.
|
||||
@ -638,7 +637,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
"StreamLocalBindMask": common.NewOption(`Sets the octal file creation mode mask (umask) used when creating a Unix-domain socket file for local or remote port forwarding. This option is only used for port forwarding to a Unix-domain socket file.
|
||||
The default value is 0177, which creates a Unix-domain socket file that is readable and writable only by the owner. Note that not all operating systems honor the file mode on Unix-domain socket files.`,
|
||||
common.PositiveNumberValue{},
|
||||
),
|
||||
),
|
||||
"StreamLocalBindUnlink": common.NewOption(`Specifies whether to remove an existing Unix-domain socket file for local or remote port forwarding before creating a new one. If the socket file already exists and StreamLocalBindUnlink is not enabled, sshd will be unable to forward the port to the Unix-domain socket file. This option is only used for port forwarding to a Unix-domain socket file.
|
||||
The argument must be yes or no. The default is no.`,
|
||||
BooleanEnumValue,
|
||||
|
@ -18,5 +18,8 @@ func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeText
|
||||
ClearDiagnostics(context, params.TextDocument.URI)
|
||||
}
|
||||
|
||||
analyzeErrors := AnalyzeValue()
|
||||
SendDiagnosticsFromAnalyzerErrors(context, params.TextDocument.URI, analyzeErrors)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user