feat: Add diagnostics; Improvements

This commit is contained in:
Myzel394 2024-07-28 20:03:11 +02:00
parent b9e8fc6e55
commit 0ae6c8d9e2
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
8 changed files with 380 additions and 88 deletions

View File

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

View File

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

View File

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

View 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 {}

View File

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

View File

@ -39,4 +39,3 @@ func QueryOpenSSHOptions(
return availableQueries, nil
}

View File

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

View File

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