Add source-local version extraction
This commit is contained in:
339
internal/engine/source_version.go
Normal file
339
internal/engine/source_version.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package engine
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
|
||||
type sourceVersionMode uint8
|
||||
|
||||
const (
|
||||
sourceVersionModeDefault sourceVersionMode = iota
|
||||
sourceVersionModeIfPresent
|
||||
sourceVersionModeRequired
|
||||
)
|
||||
|
||||
type sourceVersionRole uint8
|
||||
|
||||
const (
|
||||
sourceVersionMain sourceVersionRole = iota
|
||||
sourceVersionContribution
|
||||
)
|
||||
|
||||
type sourceVersionAncestor struct {
|
||||
path Path
|
||||
kind document.Kind
|
||||
}
|
||||
|
||||
type sourceVersionEvidence struct {
|
||||
path Path
|
||||
resolvedPath Path
|
||||
destination Path
|
||||
version Version
|
||||
location *Location
|
||||
ancestors []sourceVersionAncestor
|
||||
absence sourceVersionPathAbsence
|
||||
checked bool
|
||||
redacted bool
|
||||
}
|
||||
|
||||
type sourceVersionProcessingResult struct {
|
||||
payload *document.Node
|
||||
imports []importDeclaration
|
||||
evidence *sourceVersionEvidence
|
||||
diagnostics []Diagnostic
|
||||
}
|
||||
|
||||
type sourceVersionPathAbsence uint8
|
||||
|
||||
const (
|
||||
sourceVersionPathPresent sourceVersionPathAbsence = iota
|
||||
sourceVersionPathInvalid
|
||||
sourceVersionMemberMissing
|
||||
sourceVersionIndexUnavailable
|
||||
sourceVersionPathObstructed
|
||||
)
|
||||
|
||||
type sourceVersionPath struct {
|
||||
node *document.Node
|
||||
present bool
|
||||
absence sourceVersionPathAbsence
|
||||
nearest document.Location
|
||||
resolvedPath Path
|
||||
ancestors []sourceVersionAncestor
|
||||
}
|
||||
|
||||
func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult {
|
||||
if role != sourceVersionMain && role != sourceVersionContribution {
|
||||
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
|
||||
}
|
||||
if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired {
|
||||
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version mode is invalid")}}
|
||||
}
|
||||
if mode != sourceVersionModeDefault && (rule == nil || rule.Check == nil || !rule.Path.valid()) {
|
||||
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version checking requires a valid version rule and checker")}}
|
||||
}
|
||||
if root == nil {
|
||||
if role == sourceVersionMain && mode != sourceVersionModeDefault {
|
||||
return sourceVersionProcessingResult{diagnostics: canonicalDiagnostics(reportVersion(rule, Version{}, nil))}
|
||||
}
|
||||
return sourceVersionProcessingResult{}
|
||||
}
|
||||
|
||||
payload, imports, diagnostics := splitImportDirectives(root)
|
||||
if len(diagnostics) != 0 {
|
||||
return sourceVersionProcessingResult{diagnostics: diagnostics}
|
||||
}
|
||||
result := sourceVersionProcessingResult{payload: payload, imports: imports}
|
||||
if mode == sourceVersionModeDefault {
|
||||
return result
|
||||
}
|
||||
|
||||
resolved := resolveSourceVersionPath(root, rule.Path)
|
||||
evidence := &sourceVersionEvidence{
|
||||
path: rule.Path.clone(), resolvedPath: resolved.resolvedPath,
|
||||
destination: destination.clone(), ancestors: resolved.ancestors,
|
||||
absence: resolved.absence,
|
||||
}
|
||||
location := resolved.nearest
|
||||
if resolved.present {
|
||||
location = resolved.node.Location
|
||||
}
|
||||
if locationKnown(location) {
|
||||
public := publicLocation(location)
|
||||
evidence.location = &public
|
||||
}
|
||||
result.evidence = evidence
|
||||
|
||||
interpreted := interpretVersion(rule, root)
|
||||
evidence.version = interpreted.version
|
||||
if sourceVersionControlOverlap(root, evidence.resolvedPath) {
|
||||
evidence.redactText()
|
||||
result.payload = nil
|
||||
result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)}
|
||||
return result
|
||||
}
|
||||
if hasErrors(interpreted.diagnostics) {
|
||||
evidence.redactText()
|
||||
result.payload = nil
|
||||
result.diagnostics = canonicalDiagnostics(interpreted.diagnostics)
|
||||
return result
|
||||
}
|
||||
if !interpreted.version.Present {
|
||||
if mode == sourceVersionModeRequired {
|
||||
evidence.redactText()
|
||||
result.payload = nil
|
||||
result.diagnostics = []Diagnostic{sourceVersionMissingDiagnostic(rule, evidence)}
|
||||
return result
|
||||
}
|
||||
if role == sourceVersionContribution {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
evidence.checked = true
|
||||
result.diagnostics = canonicalDiagnostics(reportVersion(rule, interpreted.version, root))
|
||||
if hasErrors(result.diagnostics) {
|
||||
evidence.redactText()
|
||||
result.payload = nil
|
||||
return result
|
||||
}
|
||||
if role == sourceVersionContribution && interpreted.version.Present {
|
||||
if !removeSourceVersionNode(result.payload, evidence.resolvedPath) {
|
||||
evidence.redactText()
|
||||
result.payload = nil
|
||||
result.diagnostics = []Diagnostic{sourceVersionOptionsDiagnostic("selected source version could not be removed from its owned payload")}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sourceVersionOptionsDiagnostic(message string) Diagnostic {
|
||||
return Diagnostic{Code: CodeOptionsInvalid, Severity: SeverityError, Message: message}
|
||||
}
|
||||
|
||||
func sourceVersionMissingDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
|
||||
return Diagnostic{
|
||||
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
|
||||
Message: "required schema version is missing", Primary: cloneLocation(evidence.location),
|
||||
}
|
||||
}
|
||||
|
||||
func sourceVersionOverlapDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
|
||||
return Diagnostic{
|
||||
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
|
||||
Message: "schema version path overlaps a structural directive", Primary: cloneLocation(evidence.location),
|
||||
}
|
||||
}
|
||||
|
||||
func (evidence *sourceVersionEvidence) redactText() {
|
||||
if evidence == nil {
|
||||
return
|
||||
}
|
||||
evidence.version.Text = ""
|
||||
evidence.redacted = true
|
||||
}
|
||||
|
||||
func resolveSourceVersionPath(root *document.Node, path Path) sourceVersionPath {
|
||||
resolved := sourceVersionPath{absence: sourceVersionPathInvalid}
|
||||
if root == nil || !path.valid() {
|
||||
return resolved
|
||||
}
|
||||
current := root
|
||||
resolved.nearest = current.Location
|
||||
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{kind: current.Kind})
|
||||
for _, segment := range path.segments {
|
||||
switch current.Kind {
|
||||
case document.Mapping:
|
||||
if segment.kind == pathSegmentIndex {
|
||||
resolved.absence = sourceVersionPathObstructed
|
||||
return resolved
|
||||
}
|
||||
resolved.resolvedPath = resolved.resolvedPath.Key(segment.key)
|
||||
found := false
|
||||
for _, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
current = entry.Value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
resolved.absence = sourceVersionMemberMissing
|
||||
return resolved
|
||||
}
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok {
|
||||
resolved.absence = sourceVersionIndexUnavailable
|
||||
return resolved
|
||||
}
|
||||
resolved.resolvedPath = resolved.resolvedPath.Index(index)
|
||||
if index >= len(current.Sequence) {
|
||||
resolved.absence = sourceVersionIndexUnavailable
|
||||
return resolved
|
||||
}
|
||||
current = current.Sequence[index]
|
||||
default:
|
||||
resolved.absence = sourceVersionPathObstructed
|
||||
return resolved
|
||||
}
|
||||
resolved.nearest = current.Location
|
||||
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{
|
||||
path: resolved.resolvedPath.clone(), kind: current.Kind,
|
||||
})
|
||||
}
|
||||
resolved.node = current
|
||||
resolved.present = true
|
||||
resolved.absence = sourceVersionPathPresent
|
||||
return resolved
|
||||
}
|
||||
|
||||
func sourceVersionControlOverlap(root *document.Node, path Path) bool {
|
||||
if root == nil || !path.valid() {
|
||||
return false
|
||||
}
|
||||
if root.Kind == document.Mapping {
|
||||
for _, entry := range root.Mapping {
|
||||
if entry.Key == directiveKey && pathHasPrefix(path, Keys(directiveKey)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return sourceVersionInheritanceOverlap(root, Path{}, path)
|
||||
}
|
||||
|
||||
func sourceVersionInheritanceOverlap(node *document.Node, current, path Path) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
if node.Kind == document.Mapping {
|
||||
if _, found := inheritanceDirective(node); found {
|
||||
controlPath := current.Key(inheritanceKey)
|
||||
if pathHasPrefix(path, controlPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, entry := range node.Mapping {
|
||||
if entry.Key == inheritanceKey {
|
||||
continue
|
||||
}
|
||||
if sourceVersionInheritanceOverlap(entry.Value, current.Key(entry.Key), path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if node.Kind == document.Sequence {
|
||||
for index, child := range node.Sequence {
|
||||
if sourceVersionInheritanceOverlap(child, current.Index(index), path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pathHasPrefix(path, prefix Path) bool {
|
||||
if !path.valid() || !prefix.valid() || len(prefix.segments) > len(path.segments) {
|
||||
return false
|
||||
}
|
||||
for index, segment := range prefix.segments {
|
||||
candidate := path.segments[index]
|
||||
if candidate.kind != segment.kind || candidate.key != segment.key || candidate.index != segment.index {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func removeSourceVersionNode(root *document.Node, path Path) bool {
|
||||
if root == nil || !path.valid() || len(path.segments) == 0 {
|
||||
return false
|
||||
}
|
||||
current := root
|
||||
for _, segment := range path.segments[:len(path.segments)-1] {
|
||||
switch current.Kind {
|
||||
case document.Mapping:
|
||||
if segment.kind == pathSegmentIndex {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
for _, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
current = entry.Value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok || index >= len(current.Sequence) {
|
||||
return false
|
||||
}
|
||||
current = current.Sequence[index]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
last := path.segments[len(path.segments)-1]
|
||||
switch current.Kind {
|
||||
case document.Mapping:
|
||||
if last.kind == pathSegmentIndex {
|
||||
return false
|
||||
}
|
||||
for index, entry := range current.Mapping {
|
||||
if entry.Key == last.key {
|
||||
current.Mapping = append(current.Mapping[:index], current.Mapping[index+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(last)
|
||||
if !ok || index >= len(current.Sequence) {
|
||||
return false
|
||||
}
|
||||
current.Sequence = append(current.Sequence[:index], current.Sequence[index+1:]...)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user