270 lines
9.9 KiB
Go
270 lines
9.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
const directiveKey = "configkit"
|
|
|
|
type importDeclaration struct {
|
|
path string
|
|
glob string
|
|
destination Path
|
|
placement placementMode
|
|
placementPath Path
|
|
placementLocation *Location
|
|
optional bool
|
|
location Location
|
|
}
|
|
|
|
func splitImportDirectives(root *document.Node) (*document.Node, []importDeclaration, []Diagnostic) {
|
|
body := document.Clone(root)
|
|
var declarations []importDeclaration
|
|
directiveIndex := -1
|
|
for index, entry := range body.Mapping {
|
|
if entry.Key != directiveKey {
|
|
continue
|
|
}
|
|
directiveIndex = index
|
|
parsed, diagnostics := parseImportDirective(entry.Value, Keys(directiveKey))
|
|
if len(diagnostics) != 0 {
|
|
return nil, nil, diagnostics
|
|
}
|
|
declarations = parsed
|
|
break
|
|
}
|
|
if directiveIndex >= 0 {
|
|
body.Mapping = append(body.Mapping[:directiveIndex], body.Mapping[directiveIndex+1:]...)
|
|
}
|
|
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
|
|
return nil, nil, diagnostics
|
|
}
|
|
return body, declarations, nil
|
|
}
|
|
|
|
func parseImportDirective(node *document.Node, directivePath Path) ([]importDeclaration, []Diagnostic) {
|
|
if node.Kind != document.Mapping {
|
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
|
|
}
|
|
var declarations []importDeclaration
|
|
for _, entry := range node.Mapping {
|
|
fieldPath := directivePath.Key(entry.Key)
|
|
if entry.Key != "imports" {
|
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)}
|
|
}
|
|
if entry.Value.Kind != document.Sequence {
|
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)}
|
|
}
|
|
for index, item := range entry.Value.Sequence {
|
|
declaration, diagnostics := parseImportDeclaration(item, fieldPath.Index(index))
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
declarations = append(declarations, declaration)
|
|
}
|
|
}
|
|
return declarations, nil
|
|
}
|
|
|
|
func parseImportDeclaration(node *document.Node, declarationPath Path) (importDeclaration, []Diagnostic) {
|
|
if node.Kind != document.Mapping {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
|
|
}
|
|
declaration := importDeclaration{location: publicLocation(node.Location), placementPath: declarationPath.clone()}
|
|
seenTarget := false
|
|
for _, entry := range node.Mapping {
|
|
fieldPath := declarationPath.Key(entry.Key)
|
|
switch entry.Key {
|
|
case "path", "glob":
|
|
if seenTarget || entry.Value.Kind != document.String || entry.Value.Text == "" || strings.TrimSpace(entry.Value.Text) != entry.Value.Text {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration requires exactly one nonempty path or glob string", entry.Value)}
|
|
}
|
|
seenTarget = true
|
|
declaration.location = publicLocation(entry.Value.Location)
|
|
if entry.Key == "path" {
|
|
declaration.path = entry.Value.Text
|
|
} else {
|
|
declaration.glob = entry.Value.Text
|
|
}
|
|
case "optional":
|
|
if entry.Value.Kind != document.Boolean {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)}
|
|
}
|
|
declaration.optional = entry.Value.Text == "true"
|
|
case "destination":
|
|
if entry.Value.Kind != document.Sequence {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", entry.Value)}
|
|
}
|
|
relative := Path{segments: make([]pathSegment, 0, len(entry.Value.Sequence))}
|
|
for index, item := range entry.Value.Sequence {
|
|
memberPath := fieldPath.Index(index)
|
|
if item.Kind != document.String {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item)}
|
|
}
|
|
member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}}
|
|
if _, err := normalizeDestination(member); err != nil {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item)}
|
|
}
|
|
relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text})
|
|
}
|
|
normalized, err := normalizeDestination(relative)
|
|
if err != nil {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", entry.Value)}
|
|
}
|
|
declaration.destination = normalized
|
|
case "placement":
|
|
declaration.placementPath = fieldPath.clone()
|
|
placementLocation := publicLocation(entry.Value.Location)
|
|
declaration.placementLocation = &placementLocation
|
|
if entry.Value.Kind != document.String {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)}
|
|
}
|
|
switch entry.Value.Text {
|
|
case "mapping":
|
|
declaration.placement = placementModeMapping
|
|
case "sequence-item":
|
|
declaration.placement = placementModeSequenceItem
|
|
default:
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)}
|
|
}
|
|
default:
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
|
|
}
|
|
}
|
|
if !seenTarget {
|
|
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path or glob", node)}
|
|
}
|
|
return declaration, nil
|
|
}
|
|
|
|
func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
|
if node.Kind == document.Mapping {
|
|
for _, entry := range node.Mapping {
|
|
childPath := current.Key(entry.Key)
|
|
if entry.Key == directiveKey {
|
|
location := publicLocation(entry.KeyLocation)
|
|
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
|
|
}
|
|
if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
} else if node.Kind == document.Sequence {
|
|
for index, child := range node.Sequence {
|
|
if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
|
|
if rule == nil {
|
|
return nil
|
|
}
|
|
current := root
|
|
if len(rule.Path.segments) == 0 {
|
|
return current
|
|
}
|
|
for _, segment := range rule.Path.segments {
|
|
if current.Kind != document.Mapping {
|
|
return current
|
|
}
|
|
if segment.kind == pathSegmentIndex {
|
|
return nil
|
|
}
|
|
key := segment.key
|
|
index := documentEntryIndex(current.Mapping, key)
|
|
if index < 0 {
|
|
return nil
|
|
}
|
|
current = current.Mapping[index].Value
|
|
}
|
|
return current
|
|
}
|
|
|
|
func validImportPath(value string) bool {
|
|
if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") {
|
|
return false
|
|
}
|
|
return !hasImportScheme(value)
|
|
}
|
|
|
|
func hasImportScheme(value string) bool {
|
|
colon := strings.IndexByte(value, ':')
|
|
slash := strings.IndexByte(value, '/')
|
|
if colon <= 0 || (slash >= 0 && slash < colon) {
|
|
return false
|
|
}
|
|
for index := 0; index < colon; index++ {
|
|
character := value[index]
|
|
if index == 0 {
|
|
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
|
|
(character < '0' || character > '9') && character != '+' && character != '-' && character != '.' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic {
|
|
location := publicLocation(node.Location)
|
|
return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location}
|
|
}
|
|
|
|
func overlayDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
|
|
if node.Kind == document.Mapping {
|
|
for _, entry := range node.Mapping {
|
|
childPath := current.Key(entry.Key)
|
|
if entry.Key == directiveKey {
|
|
location := publicLocation(entry.KeyLocation)
|
|
return []Diagnostic{{
|
|
Code: CodeOverlayDirective, Severity: SeverityError, Path: childPath,
|
|
Message: "overlays cannot contain configuration directives", Primary: &location,
|
|
}}
|
|
}
|
|
if diagnostics := overlayDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
} else if node.Kind == document.Sequence {
|
|
for index, child := range node.Sequence {
|
|
if diagnostics := overlayDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
|
|
var diagnostics []Diagnostic
|
|
if node.Kind == document.Mapping {
|
|
for _, entry := range node.Mapping {
|
|
childPath := path.Key(entry.Key)
|
|
if entry.Key == directiveKey {
|
|
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
|
|
diagnostics = append(diagnostics, Diagnostic{
|
|
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
|
|
Message: "configuration directives require imports to be enabled", Primary: &location,
|
|
})
|
|
}
|
|
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...)
|
|
}
|
|
} else if node.Kind == document.Sequence {
|
|
for index, child := range node.Sequence {
|
|
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...)
|
|
}
|
|
}
|
|
return diagnostics
|
|
}
|