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 ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
type Value interface { type Value interface {
getTypeDescription() []string GetTypeDescription() []string
CheckIsValid(value string) error
} }
type EnumValue struct { type EnumValue struct {
@ -19,7 +21,7 @@ type EnumValue struct {
EnforceValues bool EnforceValues bool
} }
func (v EnumValue) getTypeDescription() []string { func (v EnumValue) GetTypeDescription() []string {
lines := make([]string, len(v.Values)+1) lines := make([]string, len(v.Values)+1)
lines[0] = "Enum of:" lines[0] = "Enum of:"
@ -29,12 +31,42 @@ func (v EnumValue) getTypeDescription() []string {
return lines 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{} type PositiveNumberValue struct{}
func (v PositiveNumberValue) getTypeDescription() []string { func (v PositiveNumberValue) GetTypeDescription() []string {
return []string{"Positive number"} 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 { type ArrayValue struct {
SubValue Value SubValue Value
@ -42,25 +74,57 @@ type ArrayValue struct {
AllowDuplicates bool AllowDuplicates bool
} }
func (v ArrayValue) getTypeDescription() []string { func (v ArrayValue) GetTypeDescription() []string {
subValue := v.SubValue.(Value) subValue := v.SubValue.(Value)
return append( return append(
[]string{"An Array separated by " + v.Separator + " of:"}, []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 { type OrValue struct {
Values []Value Values []Value
} }
func (v OrValue) getTypeDescription() []string { func (v OrValue) GetTypeDescription() []string {
lines := make([]string, 0) lines := make([]string, 0)
for _, subValueRaw := range v.Values { for _, subValueRaw := range v.Values {
subValue := subValueRaw.(Value) subValue := subValueRaw.(Value)
subLines := subValue.getTypeDescription() subLines := subValue.GetTypeDescription()
for index, line := range subLines { for index, line := range subLines {
if strings.HasPrefix(line, "\t*") { if strings.HasPrefix(line, "\t*") {
@ -78,21 +142,48 @@ func (v OrValue) getTypeDescription() []string {
lines..., 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{} type StringValue struct{}
func (v StringValue) getTypeDescription() []string { func (v StringValue) GetTypeDescription() []string {
return []string{"String"} return []string{"String"}
} }
func (v StringValue) CheckIsValid(value string) error {
if value == "" {
return EmptyStringError{}
}
return nil
}
type CustomValue struct { type CustomValue struct {
FetchValue func() Value FetchValue func() Value
} }
func (v CustomValue) getTypeDescription() []string { func (v CustomValue) GetTypeDescription() []string {
return []string{"Custom"} return []string{"Custom"}
} }
func (v CustomValue) CheckIsValid(value string) error {
return v.FetchValue().CheckIsValid(value)
}
type Prefix struct { type Prefix struct {
Prefix string Prefix string
Meaning string Meaning string
@ -102,8 +193,8 @@ type PrefixWithMeaningValue struct {
SubValue Value SubValue Value
} }
func (v PrefixWithMeaningValue) getTypeDescription() []string { func (v PrefixWithMeaningValue) GetTypeDescription() []string {
subDescription := v.SubValue.getTypeDescription() subDescription := v.SubValue.GetTypeDescription()
prefixDescription := Map(v.Prefixes, func(prefix Prefix) string { prefixDescription := Map(v.Prefixes, func(prefix Prefix) string {
return fmt.Sprintf("_%s_ -> %s", prefix.Prefix, prefix.Meaning) 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 type PathType uint8
const ( const (
@ -129,13 +224,13 @@ type PathValue struct {
RequiredType PathType RequiredType PathType
} }
func (v PathValue) getTypeDescription() []string { func (v PathValue) GetTypeDescription() []string {
hints := make([]string, 0) hints := make([]string, 0)
switch v.RequiredType { switch v.RequiredType {
case PathTypeExistenceOptional: case PathTypeExistenceOptional:
hints = append(hints, "Optional") hints = append(hints, "Optional")
break; break
case PathTypeFile: case PathTypeFile:
hints = append(hints, "File") hints = append(hints, "File")
case PathTypeDirectory: case PathTypeDirectory:
@ -145,13 +240,35 @@ func (v PathValue) getTypeDescription() []string {
return []string{strings.Join(hints, ", ")} 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 { type Option struct {
Documentation string Documentation string
Value Value Value Value
} }
func GetDocumentation(o *Option) protocol.MarkupContent { func GetDocumentation(o *Option) protocol.MarkupContent {
typeDescription := strings.Join(o.Value.getTypeDescription(), "\n") typeDescription := strings.Join(o.Value.GetTypeDescription(), "\n")
return protocol.MarkupContent{ return protocol.MarkupContent{
Kind: protocol.MarkupKindPlainText, Kind: protocol.MarkupKindPlainText,

View File

@ -39,10 +39,55 @@ func (e LineNotFoundError) Error() string {
} }
type ValueNotInEnumError struct { type ValueNotInEnumError struct {
availableValues []string AvailableValues []string
providedValue 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 { 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 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 return availableQueries, nil
} }

View File

@ -320,7 +320,6 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
options, _ = QueryOpenSSHOptions("HostbasedAcceptedKeyTypes") options, _ = QueryOpenSSHOptions("HostbasedAcceptedKeyTypes")
} }
return PrefixPlusMinusCaret(options) return PrefixPlusMinusCaret(options)
}, },
}, },

View File

@ -18,5 +18,8 @@ func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeText
ClearDiagnostics(context, params.TextDocument.URI) ClearDiagnostics(context, params.TextDocument.URI)
} }
analyzeErrors := AnalyzeValue()
SendDiagnosticsFromAnalyzerErrors(context, params.TextDocument.URI, analyzeErrors)
return nil return nil
} }