Select named imports before source admission
This commit is contained in:
130
internal/engine/import_selection.go
Normal file
130
internal/engine/import_selection.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type importSelectionOutcome uint8
|
||||
|
||||
const (
|
||||
importSelectionNone importSelectionOutcome = iota
|
||||
importSelectionHasCandidates
|
||||
)
|
||||
|
||||
type selectedImportCandidate struct {
|
||||
path string
|
||||
accessPath string
|
||||
site importFieldSite
|
||||
}
|
||||
|
||||
// importSelection contains lexical candidates only; admission assigns source identity later.
|
||||
type importSelection struct {
|
||||
candidates []selectedImportCandidate
|
||||
outcome importSelectionOutcome
|
||||
}
|
||||
|
||||
func selectNamedImportCandidates(access *filesystemAccess, basePath string, spec importDeclarationSpec, policy resolvedImportPolicy) (importSelection, []Diagnostic) {
|
||||
normalized := make([]selectedImportCandidate, 0, len(spec.paths))
|
||||
seen := make(map[string]struct{}, len(spec.paths))
|
||||
for index, relative := range spec.paths {
|
||||
accessPath := access.join(basePath, relative)
|
||||
candidatePath := accessPath
|
||||
if access.native == nil {
|
||||
candidatePath = filepath.ToSlash(filepath.Clean(accessPath))
|
||||
}
|
||||
if _, exists := seen[candidatePath]; exists {
|
||||
continue
|
||||
}
|
||||
seen[candidatePath] = struct{}{}
|
||||
var site importFieldSite
|
||||
if index < len(spec.pathSites) {
|
||||
site = spec.pathSites[index]
|
||||
} else {
|
||||
site = spec.fieldSites["path"]
|
||||
}
|
||||
normalized = append(normalized, selectedImportCandidate{path: candidatePath, accessPath: accessPath, site: site})
|
||||
}
|
||||
sort.Slice(normalized, func(left, right int) bool {
|
||||
return normalized[left].path < normalized[right].path
|
||||
})
|
||||
candidates := make([]selectedImportCandidate, 0, len(normalized))
|
||||
for _, candidate := range normalized {
|
||||
missing, diagnostic := validateNamedImportTarget(access, candidate, policy.required)
|
||||
if diagnostic != nil {
|
||||
return importSelection{}, []Diagnostic{*diagnostic}
|
||||
}
|
||||
if missing {
|
||||
continue
|
||||
}
|
||||
if excludedBasename(path.Base(candidate.path), policy.exclude) {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
selection := importSelection{candidates: candidates}
|
||||
if len(candidates) != 0 {
|
||||
selection.outcome = importSelectionHasCandidates
|
||||
}
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
func validateNamedImportTarget(access *filesystemAccess, candidate selectedImportCandidate, required bool) (bool, *Diagnostic) {
|
||||
if !access.contains(candidate.accessPath) {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportOutsideRoot, "import target is outside the configuration root")
|
||||
}
|
||||
canonical, err := access.canonical(candidate.accessPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if required {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportMissing, "required import target does not exist")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target cannot be accessed")
|
||||
}
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target could not be canonicalized")
|
||||
}
|
||||
if !access.contains(canonical) {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportOutsideRoot, "import target resolves outside the configuration root")
|
||||
}
|
||||
info, err := access.stat(canonical)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if required {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportMissing, "required import target does not exist")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target cannot be accessed")
|
||||
}
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target could not be inspected")
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return false, importSelectionDiagnostic(candidate.site, CodeSourceNotRegular, "source is not a regular file")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func excludedBasename(name string, patterns []string) bool {
|
||||
for _, pattern := range patterns {
|
||||
matched, err := path.Match(pattern, name)
|
||||
if err == nil && matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func importSelectionDiagnostic(site importFieldSite, code Code, message string) *Diagnostic {
|
||||
location := site.location
|
||||
return &Diagnostic{
|
||||
Code: code, Severity: SeverityError, Path: site.path.clone(),
|
||||
Message: message, Primary: &location,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user