2026-10-19 20:07:54 +00:00
package engine
import (
"errors"
"io/fs"
"path"
"path/filepath"
"sort"
2026-10-19 20:29:20 +00:00
"strings"
2026-10-19 20:07:54 +00:00
)
type importSelectionOutcome uint8
const (
importSelectionNone importSelectionOutcome = iota
importSelectionHasCandidates
)
type selectedImportCandidate struct {
path string
accessPath string
2026-10-19 20:50:47 +00:00
label string
2026-10-19 20:07:54 +00:00
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
2026-10-19 20:13:33 +00:00
if index < len ( spec . selectorSites ) {
site = spec . selectorSites [ index ]
2026-10-19 20:07:54 +00:00
} else {
site = spec . fieldSites [ "path" ]
}
2026-10-19 20:50:47 +00:00
normalized = append ( normalized , selectedImportCandidate { path : candidatePath , accessPath : accessPath , label : relativeImportLabel ( access , basePath , accessPath ) , site : site } )
2026-10-19 20:07:54 +00:00
}
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 {
2026-10-19 20:13:33 +00:00
missing , _ , diagnostic := validateNamedImportTarget ( access , candidate , policy . required , false )
2026-10-19 20:07:54 +00:00
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
}
2026-10-19 20:18:39 +00:00
func selectDirectoryImportCandidates ( access * filesystemAccess , basePath string , spec importDeclarationSpec , policy resolvedImportPolicy , limits importLimits , accounting * importAccounting ) ( importSelection , [ ] Diagnostic ) {
2026-10-19 20:13:33 +00:00
roots := make ( [ ] selectedImportCandidate , 0 , len ( spec . paths ) )
seenRoots := make ( map [ string ] struct { } , len ( spec . paths ) )
for index , relative := range spec . paths {
accessPath := access . join ( basePath , relative )
candidatePath := normalizedImportCandidatePath ( access , accessPath )
if _ , exists := seenRoots [ candidatePath ] ; exists {
continue
}
seenRoots [ candidatePath ] = struct { } { }
site := spec . fieldSites [ "dir" ]
if index < len ( spec . selectorSites ) {
site = spec . selectorSites [ index ]
}
roots = append ( roots , selectedImportCandidate { path : candidatePath , accessPath : accessPath , site : site } )
}
sort . Slice ( roots , func ( left , right int ) bool { return roots [ left ] . path < roots [ right ] . path } )
if accounting == nil {
accounting = & importAccounting { }
}
discovered := make ( [ ] selectedImportCandidate , 0 )
seenCandidates := make ( map [ string ] struct { } )
2026-10-19 21:23:47 +00:00
// Each root is traversed once. Filename patterns are tested together for
// every discovered file, and seenCandidates forms their set union across
// overlapping roots. Exclusions are applied once below as set difference.
2026-10-19 20:13:33 +00:00
for _ , root := range roots {
missing , isDirectory , diagnostic := validateNamedImportTarget ( access , root , false , true )
if diagnostic != nil {
return importSelection { } , [ ] Diagnostic { * diagnostic }
}
if missing {
continue
}
if ! isDirectory {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( root . site , CodeImportInvalid , "directory search root is not a directory" ) }
}
2026-10-19 20:29:20 +00:00
rootTarget , targetErr := access . importEntryTarget ( root . accessPath , nil , true )
if targetErr != nil {
return importSelection { } , [ ] Diagnostic { * importDirectoryTargetDiagnostic ( root . site , targetErr ) }
}
2026-10-19 20:50:47 +00:00
root . accessPath = rootTarget . identity
2026-10-19 20:18:39 +00:00
entries , missing , diagnostic := importDirectoryEntries ( access , root . accessPath , root . site , limits , accounting )
if diagnostic != nil {
return importSelection { } , [ ] Diagnostic { * diagnostic }
2026-10-19 20:13:33 +00:00
}
2026-10-19 20:18:39 +00:00
if missing {
2026-10-19 20:13:33 +00:00
continue
}
2026-10-19 20:29:20 +00:00
stack := [ ] importDirectoryFrame { { directory : root , depth : 0 , entries : entries , identity : rootTarget . identity } }
2026-10-19 21:23:47 +00:00
// Active-path cycle detection removes each directory when its frame is
// popped, so a later non-active alias remains a separate traversal.
2026-10-19 20:29:20 +00:00
activeDirectories := map [ string ] struct { } { rootTarget . identity : { } }
2026-10-19 20:18:39 +00:00
for len ( stack ) != 0 {
frameIndex := len ( stack ) - 1
frame := & stack [ frameIndex ]
if frame . next >= len ( frame . entries ) {
2026-10-19 20:29:20 +00:00
delete ( activeDirectories , frame . identity )
2026-10-19 20:18:39 +00:00
stack = stack [ : frameIndex ]
2026-10-19 20:13:33 +00:00
continue
}
2026-10-19 20:18:39 +00:00
entry := frame . entries [ frame . next ]
frame . next ++
accessPath := access . join ( frame . directory . accessPath , entry . Name ( ) )
2026-10-19 20:50:47 +00:00
lexicalPath := access . join ( frame . directory . path , entry . Name ( ) )
2026-10-19 20:13:33 +00:00
candidate := selectedImportCandidate {
2026-10-19 20:50:47 +00:00
path : normalizedImportCandidatePath ( access , lexicalPath ) , accessPath : accessPath ,
label : relativeImportLabel ( access , basePath , lexicalPath ) , site : frame . directory . site ,
2026-10-19 20:18:39 +00:00
}
2026-10-19 20:29:20 +00:00
if ! policy . hidden && strings . HasPrefix ( entry . Name ( ) , "." ) {
continue
}
target , entryErr := access . importEntryTarget ( candidate . accessPath , entry , policy . follow )
2026-10-19 20:18:39 +00:00
if entryErr != nil {
2026-10-19 20:29:20 +00:00
if errors . Is ( entryErr , fs . ErrNotExist ) {
continue
}
return importSelection { } , [ ] Diagnostic { * importDirectoryTargetDiagnostic ( frame . directory . site , entryErr ) }
}
if target . symlink && ! policy . follow {
continue
2026-10-19 20:18:39 +00:00
}
2026-10-19 20:29:20 +00:00
if target . dir {
2026-10-19 20:18:39 +00:00
if ! directoryDepthAllows ( policy , frame . depth ) {
continue
}
2026-10-19 20:29:20 +00:00
if _ , exists := activeDirectories [ target . identity ] ; exists {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( frame . directory . site , CodeImportCycle , "directory traversal cycle detected" ) }
}
2026-10-19 20:18:39 +00:00
childMissing , childIsDirectory , childDiagnostic := validateNamedImportTarget ( access , candidate , false , true )
if childDiagnostic != nil {
return importSelection { } , [ ] Diagnostic { * childDiagnostic }
}
if childMissing || ! childIsDirectory {
continue
}
childEntries , vanished , childDiagnostic := importDirectoryEntries ( access , candidate . accessPath , candidate . site , limits , accounting )
if childDiagnostic != nil {
return importSelection { } , [ ] Diagnostic { * childDiagnostic }
}
if ! vanished {
2026-10-19 20:50:47 +00:00
candidate . accessPath = target . identity
2026-10-19 20:29:20 +00:00
activeDirectories [ target . identity ] = struct { } { }
stack = append ( stack , importDirectoryFrame { directory : candidate , depth : frame . depth + 1 , entries : childEntries , identity : target . identity } )
2026-10-19 20:18:39 +00:00
}
continue
}
2026-10-19 20:50:47 +00:00
if target . identity != "" {
candidate . accessPath = target . identity
} else {
canonical , canonicalErr := access . canonical ( accessPath )
if canonicalErr != nil {
if errors . Is ( canonicalErr , fs . ErrNotExist ) {
continue
}
return importSelection { } , [ ] Diagnostic { globFilesystemDiagnostic ( canonicalErr , "directory search file could not be canonicalized" , frame . directory . site . location ) }
}
candidate . accessPath = canonical
}
2026-10-19 20:18:39 +00:00
if ! matchesImportFilename ( entry . Name ( ) , policy . pattern ) {
continue
2026-10-19 20:13:33 +00:00
}
if _ , exists := seenCandidates [ candidate . path ] ; exists {
continue
}
seenCandidates [ candidate . path ] = struct { } { }
discovered = append ( discovered , candidate )
}
}
sort . Slice ( discovered , func ( left , right int ) bool { return discovered [ left ] . path < discovered [ right ] . path } )
selected := make ( [ ] selectedImportCandidate , 0 , len ( discovered ) )
for _ , candidate := range discovered {
missing , isDirectory , diagnostic := validateNamedImportTarget ( access , candidate , false , true )
if diagnostic != nil {
return importSelection { } , [ ] Diagnostic { * diagnostic }
}
if missing || isDirectory || excludedBasename ( path . Base ( candidate . path ) , policy . exclude ) {
continue
}
selected = append ( selected , candidate )
}
if policy . required && len ( selected ) == 0 {
return importSelection { } , [ ] Diagnostic { * importSelectionDiagnostic ( spec . fieldSites [ "dir" ] , CodeImportMissing , "required directory import selected no files" ) }
}
selection := importSelection { candidates : selected }
if len ( selected ) != 0 {
selection . outcome = importSelectionHasCandidates
}
return selection , nil
}
2026-10-19 20:18:39 +00:00
type importDirectoryFrame struct {
directory selectedImportCandidate
depth int
entries [ ] fs . DirEntry
next int
2026-10-19 20:29:20 +00:00
identity string
}
func importDirectoryTargetDiagnostic ( site importFieldSite , err error ) * Diagnostic {
if isImportLinkCycle ( err ) {
return importSelectionDiagnostic ( site , CodeImportCycle , "directory traversal cycle detected" )
}
if errors . Is ( err , errImportPathOutsideRoot ) {
return importSelectionDiagnostic ( site , CodeImportOutsideRoot , "import target resolves outside the configuration root" )
}
return importSelectionDiagnostic ( site , CodeImportInvalid , "directory entry could not be inspected" )
2026-10-19 20:18:39 +00:00
}
func importDirectoryEntries ( access * filesystemAccess , directory string , site importFieldSite , limits importLimits , accounting * importAccounting ) ( [ ] fs . DirEntry , bool , * Diagnostic ) {
2026-10-19 20:50:47 +00:00
target , targetErr := access . importEntryTarget ( directory , nil , true )
if targetErr != nil {
if errors . Is ( targetErr , fs . ErrNotExist ) {
return nil , true , nil
}
return nil , false , importDirectoryTargetDiagnostic ( site , targetErr )
}
readPath := target . identity
if readPath == "" {
readPath = directory
}
2026-10-19 20:18:39 +00:00
entries := make ( [ ] fs . DirEntry , 0 )
2026-10-19 20:50:47 +00:00
err := visitBoundedImportDirectory ( access , readPath , limits . entries , accounting , func ( entry fs . DirEntry ) error {
2026-10-19 20:18:39 +00:00
entries = append ( entries , entry )
return nil
} )
if errors . Is ( err , errImportDirectoryEntryLimit ) {
return nil , false , importSelectionDiagnostic ( site , CodeImportLimitExceeded , "directory search exceeds the directory-entry visit limit" )
}
if errors . Is ( err , fs . ErrNotExist ) {
return nil , true , nil
}
if err != nil {
return nil , false , importSelectionDiagnostic ( site , CodeImportInvalid , "directory search root could not be read" )
}
sort . Slice ( entries , func ( left , right int ) bool { return entries [ left ] . Name ( ) < entries [ right ] . Name ( ) } )
return entries , false , nil
}
func directoryDepthAllows ( policy resolvedImportPolicy , currentDepth int ) bool {
if ! policy . recursive {
return false
}
childDepth := currentDepth + 1
return ! policy . depthSet || policy . depth == 0 || childDepth < policy . depth
}
2026-10-19 20:13:33 +00:00
func normalizedImportCandidatePath ( access * filesystemAccess , accessPath string ) string {
if access . native != nil {
return accessPath
}
return filepath . ToSlash ( filepath . Clean ( accessPath ) )
}
2026-10-19 20:50:47 +00:00
func relativeImportLabel ( access * filesystemAccess , basePath , candidatePath string ) string {
if access . native != nil {
if candidatePath == basePath {
return "."
}
prefix := strings . TrimSuffix ( basePath , "/" ) + "/"
if basePath == "." {
return candidatePath
}
if strings . HasPrefix ( candidatePath , prefix ) {
return strings . TrimPrefix ( candidatePath , prefix )
}
return candidatePath
}
label , err := filepath . Rel ( basePath , candidatePath )
if err == nil {
return filepath . ToSlash ( label )
}
return filepath . ToSlash ( candidatePath )
}
2026-10-19 20:13:33 +00:00
func matchesImportFilename ( name string , patterns [ ] string ) bool {
if patterns == nil {
return true
}
for _ , pattern := range patterns {
matched , err := path . Match ( pattern , name )
if err == nil && matched {
return true
}
}
return false
}
func validateNamedImportTarget ( access * filesystemAccess , candidate selectedImportCandidate , required , allowDirectory bool ) ( bool , bool , * Diagnostic ) {
2026-10-19 20:07:54 +00:00
if ! access . contains ( candidate . accessPath ) {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportOutsideRoot , "import target is outside the configuration root" )
2026-10-19 20:07:54 +00:00
}
canonical , err := access . canonical ( candidate . accessPath )
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
if required {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportMissing , "required import target does not exist" )
2026-10-19 20:07:54 +00:00
}
2026-10-19 20:13:33 +00:00
return true , false , nil
2026-10-19 20:07:54 +00:00
}
if errors . Is ( err , fs . ErrPermission ) {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportInvalid , "import target cannot be accessed" )
2026-10-19 20:07:54 +00:00
}
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportInvalid , "import target could not be canonicalized" )
2026-10-19 20:07:54 +00:00
}
if ! access . contains ( canonical ) {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportOutsideRoot , "import target resolves outside the configuration root" )
2026-10-19 20:07:54 +00:00
}
info , err := access . stat ( canonical )
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
if required {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportMissing , "required import target does not exist" )
2026-10-19 20:07:54 +00:00
}
2026-10-19 20:13:33 +00:00
return true , false , nil
2026-10-19 20:07:54 +00:00
}
if errors . Is ( err , fs . ErrPermission ) {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportInvalid , "import target cannot be accessed" )
2026-10-19 20:07:54 +00:00
}
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeImportInvalid , "import target could not be inspected" )
}
if info . IsDir ( ) && allowDirectory {
return false , true , nil
2026-10-19 20:07:54 +00:00
}
if ! info . Mode ( ) . IsRegular ( ) {
2026-10-19 20:13:33 +00:00
return false , false , importSelectionDiagnostic ( candidate . site , CodeSourceNotRegular , "source is not a regular file" )
2026-10-19 20:07:54 +00:00
}
2026-10-19 20:13:33 +00:00
return false , false , nil
2026-10-19 20:07:54 +00:00
}
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 ,
}
}