458 lines
15 KiB
Go
458 lines
15 KiB
Go
package engine
|
|
|
|
import (
|
|
"path"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type importSelector uint8
|
|
|
|
const (
|
|
importSelectorPath importSelector = iota + 1
|
|
importSelectorDirectory
|
|
importSelectorGlob
|
|
)
|
|
|
|
type importFieldSite struct {
|
|
path Path
|
|
location Location
|
|
}
|
|
|
|
// importDeclarationSpec preserves authored presence until discovery policy is resolved.
|
|
type importDeclarationSpec struct {
|
|
path Path
|
|
location Location
|
|
selector importSelector
|
|
paths []string
|
|
selectorSites []importFieldSite
|
|
glob string
|
|
globSegments []string
|
|
required *bool
|
|
recursive *bool
|
|
follow *bool
|
|
hidden *bool
|
|
pattern []string
|
|
patternSet bool
|
|
exclude []string
|
|
depth int
|
|
depthSet bool
|
|
destination Path
|
|
placement placementMode
|
|
fieldSites map[string]importFieldSite
|
|
}
|
|
|
|
// ImportDefaults transports root-owned load defaults across the facade
|
|
// boundary. Load options snapshot this value before source acquisition.
|
|
type ImportDefaults struct {
|
|
// Required controls absence handling when non-nil.
|
|
Required *bool
|
|
// Follow controls symlink following when non-nil.
|
|
Follow *bool
|
|
// Hidden controls hidden-entry discovery when non-nil.
|
|
Hidden *bool
|
|
// Recursive controls descendant directory traversal when non-nil.
|
|
Recursive *bool
|
|
// Pattern filters directory-search filenames; nil and empty differ.
|
|
Pattern []string
|
|
}
|
|
type resolvedImportPolicy struct {
|
|
required bool
|
|
follow bool
|
|
hidden bool
|
|
recursive bool
|
|
pattern []string
|
|
depth int
|
|
depthSet bool
|
|
exclude []string
|
|
destination Path
|
|
placement placementMode
|
|
}
|
|
|
|
func snapshotImportDefaults(value ImportDefaults) ImportDefaults {
|
|
return ImportDefaults{
|
|
Required: cloneBool(value.Required),
|
|
Follow: cloneBool(value.Follow),
|
|
Hidden: cloneBool(value.Hidden),
|
|
Recursive: cloneBool(value.Recursive),
|
|
Pattern: cloneStrings(value.Pattern),
|
|
}
|
|
}
|
|
|
|
func cloneBool(value *bool) *bool {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
cloned := *value
|
|
return &cloned
|
|
}
|
|
|
|
func cloneStrings(values []string) []string {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
return append(make([]string, 0, len(values)), values...)
|
|
}
|
|
|
|
func validateImportDefaults(value ImportDefaults) []Diagnostic {
|
|
for _, pattern := range value.Pattern {
|
|
if _, err := path.Match(pattern, ""); err != nil {
|
|
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "import default pattern is invalid")}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveImportPolicy(spec importDeclarationSpec, defaults ImportDefaults) (resolvedImportPolicy, []Diagnostic) {
|
|
defaults = snapshotImportDefaults(defaults)
|
|
policy := resolvedImportPolicy{
|
|
required: boolOr(spec.required, defaults.Required, false),
|
|
follow: true,
|
|
hidden: true,
|
|
recursive: false,
|
|
destination: spec.destination.clone(),
|
|
placement: spec.placement,
|
|
depth: spec.depth,
|
|
depthSet: spec.depthSet,
|
|
exclude: cloneStrings(spec.exclude),
|
|
}
|
|
if spec.selector == importSelectorDirectory || spec.selector == importSelectorGlob {
|
|
policy.follow = boolOr(spec.follow, defaults.Follow, true)
|
|
policy.hidden = boolOr(spec.hidden, defaults.Hidden, true)
|
|
}
|
|
if spec.selector == importSelectorDirectory {
|
|
policy.recursive = boolOr(spec.recursive, defaults.Recursive, false)
|
|
if spec.patternSet {
|
|
policy.pattern = cloneStrings(spec.pattern)
|
|
} else {
|
|
policy.pattern = cloneStrings(defaults.Pattern)
|
|
}
|
|
}
|
|
if spec.depthSet && !policy.recursive {
|
|
site := spec.fieldSites["depth"]
|
|
location := site.location
|
|
return resolvedImportPolicy{}, []Diagnostic{{
|
|
Code: CodeImportInvalid, Severity: SeverityError,
|
|
Path: site.path.clone(), Message: "depth requires effective recursive discovery",
|
|
Primary: &location,
|
|
}}
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func boolOr(explicit, application *bool, fallback bool) bool {
|
|
if explicit != nil {
|
|
return *explicit
|
|
}
|
|
if application != nil {
|
|
return *application
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func parseImportDeclarations(node *document.Node, declarationPath Path) ([]importDeclarationSpec, []Diagnostic) {
|
|
if node == nil || node.Kind != document.Sequence {
|
|
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "imports must be a sequence", node)}
|
|
}
|
|
var specs []importDeclarationSpec
|
|
var diagnostics []Diagnostic
|
|
for index, declarationNode := range node.Sequence {
|
|
spec, issues := parseImportDeclarationSpec(declarationNode, declarationPath.Index(index))
|
|
if len(issues) != 0 {
|
|
diagnostics = append(diagnostics, issues...)
|
|
continue
|
|
}
|
|
specs = append(specs, spec)
|
|
}
|
|
return specs, canonicalDiagnostics(diagnostics)
|
|
}
|
|
|
|
func parseImportDeclarationSpec(node *document.Node, declarationPath Path) (importDeclarationSpec, []Diagnostic) {
|
|
if node.Kind != document.Mapping {
|
|
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
|
|
}
|
|
spec := importDeclarationSpec{
|
|
path: declarationPath.clone(),
|
|
location: publicLocation(node.Location),
|
|
placement: placementModeMapping,
|
|
fieldSites: make(map[string]importFieldSite, len(node.Mapping)),
|
|
}
|
|
selectors := 0
|
|
selectorIndex := -1
|
|
for index, entry := range node.Mapping {
|
|
if selectorForField(entry.Key) != 0 {
|
|
selectors++
|
|
selectorIndex = index
|
|
}
|
|
}
|
|
if selectors != 1 {
|
|
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path, dir, or glob selector", node)}
|
|
}
|
|
selectorEntry := node.Mapping[selectorIndex]
|
|
spec.selector = selectorForField(selectorEntry.Key)
|
|
for _, entry := range node.Mapping {
|
|
fieldPath := declarationPath.Key(entry.Key)
|
|
spec.fieldSites[entry.Key] = importFieldSite{path: fieldPath.clone(), location: publicLocation(entry.Value.Location)}
|
|
if selectorForField(entry.Key) != 0 {
|
|
continue
|
|
}
|
|
if !importFieldApplies(entry.Key, spec.selector) {
|
|
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not supported by this selector", entry.Value)}
|
|
}
|
|
switch entry.Key {
|
|
case "required", "recursive", "follow", "hidden":
|
|
if diagnostic := parseImportBooleanAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
case "pattern":
|
|
if diagnostic := parseImportWildcardAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
case "exclude":
|
|
if diagnostic := parseImportWildcardAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
case "depth":
|
|
depth, ok := parseImportDepth(entry.Value)
|
|
if !ok {
|
|
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "depth must be a nonnegative integer", entry.Value)}
|
|
}
|
|
spec.depth, spec.depthSet = depth, true
|
|
case "destination":
|
|
destination, diagnostic := parseImportDestination(entry.Value, fieldPath)
|
|
if diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
spec.destination = destination
|
|
case "placement":
|
|
placement, diagnostic := parseImportPlacement(entry.Value, fieldPath)
|
|
if diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
spec.placement = placement
|
|
default:
|
|
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
|
|
}
|
|
}
|
|
|
|
selectorPath := declarationPath.Key(selectorEntry.Key)
|
|
spec.fieldSites[selectorEntry.Key] = importFieldSite{path: selectorPath.clone(), location: publicLocation(selectorEntry.Value.Location)}
|
|
if diagnostic := parseImportSelectorValue(&spec, selectorEntry.Key, selectorEntry.Value, selectorPath); diagnostic != nil {
|
|
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
func parseImportBooleanAssignment(spec *importDeclarationSpec, field string, node *document.Node, fieldPath Path) *Diagnostic {
|
|
if node.Kind != document.Boolean {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, field+" must be a boolean", node)
|
|
return &diagnostic
|
|
}
|
|
value := node.Text == "true"
|
|
switch field {
|
|
case "required":
|
|
spec.required = &value
|
|
case "recursive":
|
|
spec.recursive = &value
|
|
case "follow":
|
|
spec.follow = &value
|
|
case "hidden":
|
|
spec.hidden = &value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseImportWildcardAssignment(spec *importDeclarationSpec, field string, node *document.Node, fieldPath Path) *Diagnostic {
|
|
values, origins, ok, badIndex := parseStringSet(node)
|
|
if !ok {
|
|
diagnostic := stringSetDiagnostic(node, fieldPath, field, badIndex)
|
|
return &diagnostic
|
|
}
|
|
if badIndex, ok := invalidWildcard(values); ok {
|
|
badPath, badNode := listMemberSite(node, fieldPath, origins[badIndex])
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, badPath, field+" contains an invalid wildcard expression", badNode)
|
|
return &diagnostic
|
|
}
|
|
switch field {
|
|
case "pattern":
|
|
spec.pattern, spec.patternSet = values, true
|
|
case "exclude":
|
|
spec.exclude = values
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseImportPlacement(node *document.Node, fieldPath Path) (placementMode, *Diagnostic) {
|
|
if node.Kind != document.String {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node)
|
|
return placementModeMapping, &diagnostic
|
|
}
|
|
switch node.Text {
|
|
case "mapping":
|
|
return placementModeMapping, nil
|
|
case "sequence-item":
|
|
return placementModeSequenceItem, nil
|
|
default:
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node)
|
|
return placementModeMapping, &diagnostic
|
|
}
|
|
}
|
|
|
|
func parseImportSelectorValue(spec *importDeclarationSpec, field string, node *document.Node, selectorPath Path) *Diagnostic {
|
|
switch spec.selector {
|
|
case importSelectorPath, importSelectorDirectory:
|
|
values, origins, ok, badIndex := parseStringSet(node)
|
|
if !ok {
|
|
diagnostic := stringSetDiagnostic(node, selectorPath, field, badIndex)
|
|
return &diagnostic
|
|
}
|
|
for index, value := range values {
|
|
if !validImportPath(value) {
|
|
badPath, badNode := listMemberSite(node, selectorPath, origins[index])
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, badPath, field+" contains an invalid source path", badNode)
|
|
return &diagnostic
|
|
}
|
|
badPath, badNode := listMemberSite(node, selectorPath, origins[index])
|
|
spec.selectorSites = append(spec.selectorSites, importFieldSite{path: badPath.clone(), location: publicLocation(badNode.Location)})
|
|
}
|
|
spec.paths = values
|
|
case importSelectorGlob:
|
|
if node.Kind != document.String || !validImportPath(node.Text) {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob must be a valid nonempty path pattern string", node)
|
|
return &diagnostic
|
|
}
|
|
segments, ok := validGlobPattern(node.Text)
|
|
if !ok {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob pattern is invalid", node)
|
|
return &diagnostic
|
|
}
|
|
spec.glob, spec.globSegments = node.Text, segments
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func selectorForField(field string) importSelector {
|
|
switch field {
|
|
case "path":
|
|
return importSelectorPath
|
|
case "dir":
|
|
return importSelectorDirectory
|
|
case "glob":
|
|
return importSelectorGlob
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func importFieldApplies(field string, selector importSelector) bool {
|
|
switch field {
|
|
case "required", "exclude", "destination", "placement":
|
|
return true
|
|
case "pattern", "recursive", "depth":
|
|
return selector == importSelectorDirectory
|
|
case "follow", "hidden":
|
|
return selector == importSelectorDirectory || selector == importSelectorGlob
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func parseStringSet(node *document.Node) ([]string, []int, bool, int) {
|
|
if node.Kind == document.String {
|
|
return []string{node.Text}, []int{-1}, true, -1
|
|
}
|
|
if node.Kind != document.Sequence {
|
|
return nil, nil, false, -1
|
|
}
|
|
values := make([]string, 0, len(node.Sequence))
|
|
origins := make([]int, 0, len(node.Sequence))
|
|
seen := make(map[string]struct{}, len(node.Sequence))
|
|
for index, item := range node.Sequence {
|
|
if item.Kind != document.String {
|
|
return nil, nil, false, index
|
|
}
|
|
if _, exists := seen[item.Text]; exists {
|
|
continue
|
|
}
|
|
seen[item.Text] = struct{}{}
|
|
values = append(values, item.Text)
|
|
origins = append(origins, index)
|
|
}
|
|
return values, origins, true, -1
|
|
}
|
|
|
|
func stringSetDiagnostic(node *document.Node, fieldPath Path, field string, badIndex int) Diagnostic {
|
|
if badIndex >= 0 {
|
|
badPath, item := listMemberSite(node, fieldPath, badIndex)
|
|
return nodeImportDiagnostic(CodeImportInvalid, badPath, field+" members must be strings", item)
|
|
}
|
|
return nodeImportDiagnostic(CodeImportInvalid, fieldPath, field+" must be a string or a flat list of strings", node)
|
|
}
|
|
|
|
func invalidWildcard(values []string) (int, bool) {
|
|
for index, value := range values {
|
|
if _, err := path.Match(value, ""); err != nil {
|
|
return index, true
|
|
}
|
|
}
|
|
return -1, false
|
|
}
|
|
|
|
func listMemberSite(node *document.Node, fieldPath Path, index int) (Path, *document.Node) {
|
|
if node.Kind != document.Sequence || index < 0 {
|
|
return fieldPath, node
|
|
}
|
|
return fieldPath.Index(index), node.Sequence[index]
|
|
}
|
|
|
|
func parseImportDepth(node *document.Node) (int, bool) {
|
|
if node.Kind != document.Number || node.Text == "" {
|
|
return 0, false
|
|
}
|
|
if node.Text == "-0" {
|
|
return 0, true
|
|
}
|
|
var value uint64
|
|
maxInt := uint64(^uint(0) >> 1)
|
|
for index := 0; index < len(node.Text); index++ {
|
|
character := node.Text[index]
|
|
if character < '0' || character > '9' {
|
|
return 0, false
|
|
}
|
|
digit := uint64(character - '0')
|
|
if value > (maxInt-digit)/10 {
|
|
value = maxInt
|
|
continue // Saturate the bound, but validate every remaining character.
|
|
}
|
|
value = value*10 + digit
|
|
}
|
|
return int(value), true
|
|
}
|
|
|
|
func parseImportDestination(node *document.Node, fieldPath Path) (Path, *Diagnostic) {
|
|
if node.Kind != document.Sequence {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", node)
|
|
return Path{}, &diagnostic
|
|
}
|
|
relative := Path{segments: make([]pathSegment, 0, len(node.Sequence))}
|
|
for index, item := range node.Sequence {
|
|
memberPath := fieldPath.Index(index)
|
|
if item.Kind != document.String {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item)
|
|
return Path{}, &diagnostic
|
|
}
|
|
member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}}
|
|
if _, err := normalizeDestination(member); err != nil {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item)
|
|
return Path{}, &diagnostic
|
|
}
|
|
relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text})
|
|
}
|
|
normalized, err := normalizeDestination(relative)
|
|
if err != nil {
|
|
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", node)
|
|
return Path{}, &diagnostic
|
|
}
|
|
return normalized, nil
|
|
}
|