131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
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,
|
|
}
|
|
}
|