mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-19 07:25:27 +02:00
feat: Add diagnostics; Improvements
This commit is contained in:
parent
b9e8fc6e55
commit
0ae6c8d9e2
@ -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,
|
||||||
|
@ -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, ","))
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
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
|
return availableQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user