2026-09-12 15:58:06 +00:00
package engine
import (
"bytes"
"errors"
"io/fs"
"strconv"
"strings"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml"
)
const (
maxImportDepth = 64
maxImportSources = 1024
maxGlobEntries = 1_000_000
)
type importLimits struct {
depth int
sources int
bytes int64
nodes int
entries int
}
func defaultImportLimits ( ) importLimits {
return importLimits {
depth : maxImportDepth , sources : maxImportSources ,
bytes : maxTotalBytes , nodes : maxDocumentNodes , entries : maxGlobEntries ,
}
}
type importRoute struct {
canonical string
locations [ ] Location
}
2026-09-29 17:29:01 +00:00
type importAccounting struct {
sources int
bytes int64
nodes int
entries int
}
2026-09-12 15:58:06 +00:00
type importTraversal struct {
2026-10-15 15:33:40 +00:00
access * filesystemAccess
accounting * importAccounting
limits importLimits
rule * VersionRule
versionState * sourceVersionState
2026-09-12 15:58:06 +00:00
2026-10-16 15:48:36 +00:00
scope acquisitionScope
sources [ ] SourceInfo
active map [ resourceIdentity ] int
visited map [ resourceIdentity ] importRoute
stack [ ] importRoute
2026-09-12 15:58:06 +00:00
}
func loadImportedDocuments ( source Source , context * FilesystemContext , main parsedSource , rule * VersionRule , limits importLimits ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-14 13:19:31 +00:00
return loadImportedDocumentsTracked ( source , context , main , Path { } , rule , limits , & importAccounting { } , nil , nil )
2026-09-29 17:52:20 +00:00
}
2026-10-14 13:19:31 +00:00
func loadImportedDocumentsTracked ( source Source , context * FilesystemContext , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-15 15:33:40 +00:00
return loadImportedDocumentsTrackedWithVersion ( source , context , main , destination , rule , limits , accounting , visited , hostToken , nil )
}
func loadImportedDocumentsTrackedWithVersion ( source Source , context * FilesystemContext , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken , versionState * sourceVersionState ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-15 15:40:11 +00:00
var processing * sourceVersionProcessingResult
if versionState != nil && versionState . mode != sourceVersionModeDefault {
processing = versionState . mainProcessing
}
return loadImportedDocumentsTrackedWithProcessing ( source , context , main , destination , rule , limits , accounting , visited , hostToken , versionState , processing )
}
func loadImportedDocumentsTrackedWithProcessing ( source Source , context * FilesystemContext , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , hostToken * namespaceToken , versionState * sourceVersionState , processing * sourceVersionProcessingResult ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-09-29 17:36:20 +00:00
if nativeContext ( source , context ) {
access , basePath , mainPath , diagnostic := nativeFilesystemAccess ( source , context )
if diagnostic != nil {
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { * diagnostic }
}
2026-10-15 15:40:11 +00:00
return loadImportedWithAccessTrackedAndProcessing ( access , basePath , mainPath , main , destination , rule , limits , accounting , visited , versionState , processing )
2026-09-29 17:29:01 +00:00
}
2026-09-12 15:58:06 +00:00
rootPath , basePath , mainPath , diagnostic := importFilesystem ( source , context , main )
if diagnostic != nil {
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { * diagnostic }
}
2026-09-29 17:29:01 +00:00
access , err := openHostAccess ( rootPath )
2026-09-12 15:58:06 +00:00
if err != nil {
diagnostic := globFilesystemDiagnostic ( err , "configuration root could not be opened" , Location { } )
diagnostic . Primary = nil
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { diagnostic }
}
2026-09-29 17:52:20 +00:00
if hostToken != nil {
access . token = hostToken
}
2026-10-15 15:40:11 +00:00
return loadImportedWithAccessTrackedAndProcessing ( access , basePath , mainPath , main , destination , rule , limits , accounting , visited , versionState , processing )
2026-09-29 17:29:01 +00:00
}
2026-09-12 15:58:06 +00:00
2026-09-29 17:29:01 +00:00
func loadImportedWithAccess ( access * filesystemAccess , basePath , mainPath string , main parsedSource , rule * VersionRule , limits importLimits , accounting * importAccounting ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-14 13:19:31 +00:00
return loadImportedWithAccessTracked ( access , basePath , mainPath , main , Path { } , rule , limits , accounting , nil )
2026-09-29 17:52:20 +00:00
}
2026-10-14 13:19:31 +00:00
func loadImportedWithAccessTracked ( access * filesystemAccess , basePath , mainPath string , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-15 15:33:40 +00:00
return loadImportedWithAccessTrackedWithVersion ( access , basePath , mainPath , main , destination , rule , limits , accounting , visited , nil )
}
func loadImportedWithAccessTrackedWithVersion ( access * filesystemAccess , basePath , mainPath string , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , versionState * sourceVersionState ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-10-15 15:40:11 +00:00
var processing * sourceVersionProcessingResult
if versionState != nil && versionState . mode != sourceVersionModeDefault {
processing = versionState . mainProcessing
}
return loadImportedWithAccessTrackedAndProcessing ( access , basePath , mainPath , main , destination , rule , limits , accounting , visited , versionState , processing )
}
func loadImportedWithAccessTrackedAndProcessing ( access * filesystemAccess , basePath , mainPath string , main parsedSource , destination Path , rule * VersionRule , limits importLimits , accounting * importAccounting , visited map [ resourceIdentity ] importRoute , versionState * sourceVersionState , processing * sourceVersionProcessingResult ) ( * composedDocument , [ ] SourceInfo , [ ] Diagnostic ) {
2026-09-29 17:29:01 +00:00
if ! access . contains ( basePath ) || ( mainPath != "" && ! access . contains ( mainPath ) ) {
_ = access . close ( )
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "selected source and import base must stay inside the configuration root" , nil ) }
}
for _ , directory := range [ ] string { access . rootPath , basePath } {
info , err := access . stat ( directory )
if err != nil || ! info . IsDir ( ) {
_ = access . close ( )
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { importDiagnostic ( CodeImportInvalid , "configuration root and import base must be directories" , nil ) }
}
}
2026-09-12 15:58:06 +00:00
mainInfo := cloneSourceInfo ( * main . source )
2026-10-14 13:19:31 +00:00
mainInfo . Destination = destination . clone ( )
2026-09-12 15:58:06 +00:00
if mainPath != "" {
mainInfo . Path = mainPath
}
2026-09-29 17:52:20 +00:00
mainKey := access . identity ( mainPath )
if mainPath == "" {
mainKey = access . syntheticIdentity ( )
}
if previous , repeated := visited [ mainKey ] ; repeated {
_ = access . close ( )
return nil , sourceInfos ( main . source ) , [ ] Diagnostic { importRepeatedDiagnostic ( Location { SourceID : main . source . ID } , previous , importRoute { } ) }
}
2026-09-29 17:29:01 +00:00
accounting . sources ++
accounting . bytes += main . bytes
accounting . nodes += countInputNodes ( main . document )
2026-09-12 15:58:06 +00:00
state := importTraversal {
2026-10-15 15:33:40 +00:00
access : access , accounting : accounting , limits : limits , rule : rule , versionState : versionState ,
2026-10-16 15:48:36 +00:00
scope : acquisitionScope { destination : destination . clone ( ) } ,
sources : [ ] SourceInfo { mainInfo } ,
active : make ( map [ resourceIdentity ] int ) , visited : visited ,
2026-09-29 17:52:20 +00:00
}
if state . visited == nil {
state . visited = make ( map [ resourceIdentity ] importRoute )
2026-09-12 15:58:06 +00:00
}
if diagnostic := state . checkInitialLimits ( ) ; diagnostic != nil {
2026-09-29 17:29:01 +00:00
_ = access . close ( )
2026-09-12 15:58:06 +00:00
return nil , cloneSourceInfos ( state . sources ) , [ ] Diagnostic { * diagnostic }
}
mainIdentity := mainPath
if mainIdentity == "" {
mainIdentity = "<selected-source>"
}
mainRoute := importRoute { canonical : mainIdentity }
2026-09-29 17:29:01 +00:00
state . active [ mainKey ] = 0
state . visited [ mainKey ] = mainRoute
2026-09-12 15:58:06 +00:00
state . stack = append ( state . stack , mainRoute )
2026-10-15 15:33:40 +00:00
var body * document . Node
var declarations [ ] importDeclaration
var diagnostics [ ] Diagnostic
2026-10-15 15:40:11 +00:00
if processing != nil {
body = processing . payload
declarations = processing . imports
2026-10-15 15:33:40 +00:00
} else {
body , declarations , diagnostics = splitImportDirectives ( main . document )
}
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) == 0 {
2026-10-14 17:21:07 +00:00
_ , placed , err := placeDocument ( body , destination , body . Location , maxDocumentDepth , maxDocumentNodes )
placementNodes := destinationWrapperNodeCount ( destination )
2026-10-14 13:19:31 +00:00
if err != nil {
diagnostics = [ ] Diagnostic { sourceDiagnostic ( CodeSourceLimitExceeded , main . source . ID , "configuration exceeds the source depth or node limit after destination placement" ) }
2026-10-14 13:31:32 +00:00
} else if accounting . nodes + placementNodes > limits . nodes {
diagnostics = [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the aggregate node limit" , nil ) }
2026-10-14 13:19:31 +00:00
} else {
2026-10-14 13:31:32 +00:00
accounting . nodes += placementNodes
2026-10-16 15:48:36 +00:00
state . scope . contributions = append ( state . scope . contributions , acquiredContribution {
source : cloneSourceInfo ( mainInfo ) , payload : body , document : placed ,
destination : destination . clone ( ) , processing : processing ,
} )
2026-10-14 17:21:07 +00:00
diagnostics = state . walkDeclarations ( basePath , declarations , 0 , destination )
2026-10-14 13:19:31 +00:00
}
2026-09-12 15:58:06 +00:00
}
2026-09-29 17:29:01 +00:00
closeErr := access . close ( )
2026-09-12 15:58:06 +00:00
if len ( diagnostics ) != 0 {
return nil , cloneSourceInfos ( state . sources ) , diagnostics
}
if closeErr != nil {
return nil , cloneSourceInfos ( state . sources ) , [ ] Diagnostic { importDiagnostic ( CodeImportRead , "configuration root could not be closed" , nil ) }
}
2026-10-16 15:48:36 +00:00
documents := make ( [ ] * document . Node , 0 , len ( state . scope . contributions ) )
for _ , contribution := range state . scope . contributions {
documents = append ( documents , contribution . document )
}
composed , diagnostics := composeDocuments ( documents ... )
2026-09-12 15:58:06 +00:00
if composed != nil {
2026-10-16 15:48:36 +00:00
composed . acquisition = [ ] acquisitionScope { cloneAcquisitionScope ( state . scope ) }
2026-09-29 17:29:01 +00:00
composed . inputBytes = accounting . bytes
composed . inputNodes = accounting . nodes
2026-09-12 15:58:06 +00:00
}
return composed , cloneSourceInfos ( state . sources ) , diagnostics
}
func ( s * importTraversal ) checkInitialLimits ( ) * Diagnostic {
2026-09-29 17:29:01 +00:00
if s . accounting . sources > s . limits . sources {
2026-09-12 15:58:06 +00:00
value := importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the source-count limit" , nil )
return & value
}
2026-09-29 17:29:01 +00:00
if s . accounting . bytes > s . limits . bytes {
2026-09-12 15:58:06 +00:00
value := importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the aggregate byte limit" , nil )
return & value
}
2026-09-29 17:29:01 +00:00
if s . accounting . nodes > s . limits . nodes {
2026-09-12 15:58:06 +00:00
value := importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the aggregate node limit" , nil )
return & value
}
return nil
}
2026-10-14 17:21:07 +00:00
func ( s * importTraversal ) walkDeclarations ( basePath string , declarations [ ] importDeclaration , depth int , parentDestination Path ) [ ] Diagnostic {
2026-09-12 15:58:06 +00:00
for _ , declaration := range declarations {
2026-10-14 17:21:07 +00:00
childDestination := appendDestination ( parentDestination , declaration . destination )
2026-09-12 15:58:06 +00:00
if declaration . glob != "" {
matches , diagnostics := s . expandImportGlob ( basePath , declaration )
if len ( diagnostics ) != 0 {
return diagnostics
}
if len ( matches ) == 0 {
if declaration . optional {
continue
}
location := declaration . location
return [ ] Diagnostic { importDiagnostic ( CodeImportMissing , "required import glob matched no files" , & location ) }
}
for _ , match := range matches {
matched := declaration
matched . path = match
matched . glob = ""
2026-10-16 15:48:36 +00:00
if diagnostics := s . visitPath ( basePath , matched , childDestination , declaration . destination , depth + 1 ) ; len ( diagnostics ) != 0 {
2026-09-12 15:58:06 +00:00
return diagnostics
}
}
continue
}
2026-10-16 15:48:36 +00:00
if diagnostics := s . visitPath ( basePath , declaration , childDestination , declaration . destination , depth + 1 ) ; len ( diagnostics ) != 0 {
2026-09-12 15:58:06 +00:00
return diagnostics
}
}
return nil
}
2026-10-16 15:48:36 +00:00
func ( s * importTraversal ) visitPath ( basePath string , declaration importDeclaration , destination , relativeDestination Path , depth int ) [ ] Diagnostic {
2026-09-12 15:58:06 +00:00
location := declaration . location
if depth > s . limits . depth {
return [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the active import-depth limit" , & location ) }
}
if ! validImportPath ( declaration . path ) {
return [ ] Diagnostic { importDiagnostic ( CodeImportInvalid , "import path must be a nonempty slash-relative path without a network scheme" , & location ) }
}
2026-09-29 17:29:01 +00:00
candidate := s . access . join ( basePath , declaration . path )
if ! s . access . contains ( candidate ) {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "import target is outside the configuration root" , & location ) }
}
2026-09-29 17:29:01 +00:00
canonical , err := s . access . canonical ( candidate )
2026-09-12 15:58:06 +00:00
if err != nil {
if declaration . optional && errors . Is ( err , fs . ErrNotExist ) {
return nil
}
code := CodeImportInvalid
message := "import target could not be canonicalized"
if errors . Is ( err , fs . ErrNotExist ) {
code , message = CodeImportMissing , "required import target does not exist"
} else if errors . Is ( err , fs . ErrPermission ) {
message = "import target cannot be accessed"
}
return [ ] Diagnostic { importDiagnostic ( code , message , & location ) }
}
2026-09-29 17:29:01 +00:00
if ! s . access . contains ( canonical ) {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "import target resolves outside the configuration root" , & location ) }
}
currentRoute := s . currentRoute ( location )
2026-09-29 17:29:01 +00:00
identity := s . access . identity ( canonical )
if activeIndex , ok := s . active [ identity ] ; ok {
2026-09-12 15:58:06 +00:00
cycle := append ( [ ] importRoute ( nil ) , s . stack [ activeIndex : ] ... )
return [ ] Diagnostic { importCycleDiagnostic ( canonical , location , cycle ) }
}
2026-09-29 17:29:01 +00:00
if previous , ok := s . visited [ identity ] ; ok {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importRepeatedDiagnostic ( location , previous , currentRoute ) }
}
2026-09-29 17:29:01 +00:00
if s . accounting . sources + 1 > s . limits . sources {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the source-count limit" , & location ) }
}
2026-09-29 17:29:01 +00:00
if _ , err := s . access . relative ( canonical ) ; err != nil {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importDiagnostic ( CodeImportOutsideRoot , "import target could not be addressed inside the configuration root" , & location ) }
}
2026-09-29 17:29:01 +00:00
sourceID := "source-" + strconv . Itoa ( s . accounting . sources + 1 )
2026-09-29 17:36:20 +00:00
kind := SourceKindFile
label := declaration . path
if s . access . native != nil {
kind = SourceKindFilesystem
label = s . access . label + "/" + canonical
}
2026-10-14 17:21:07 +00:00
info := SourceInfo { ID : sourceID , Label : label , Path : canonical , Destination : destination . clone ( ) , Layer : LayerBase , Kind : kind , ImportedFrom : & location }
2026-09-29 17:58:15 +00:00
if kind == SourceKindFilesystem {
info . namespace = s . access . token
}
2026-09-12 15:58:06 +00:00
s . sources = append ( s . sources , info )
2026-09-29 17:29:01 +00:00
data , skipped , diagnostic := s . readImport ( canonical , sourceID , declaration . optional )
2026-09-12 15:58:06 +00:00
if diagnostic != nil {
return [ ] Diagnostic { * diagnostic }
}
if skipped {
s . sources = s . sources [ : len ( s . sources ) - 1 ]
return nil
}
2026-09-29 17:29:01 +00:00
s . accounting . sources ++
2026-09-12 15:58:06 +00:00
root , issues := yamladapter . Parse ( bytes . NewReader ( data ) , sourceID , yamladapter . Limits {
Depth : maxDocumentDepth , Nodes : maxDocumentNodes ,
} )
if len ( issues ) != 0 {
return parserDiagnostics ( sourceID , issues )
}
nodeCount := countInputNodes ( root )
2026-09-29 17:29:01 +00:00
if s . accounting . nodes + nodeCount > s . limits . nodes {
2026-09-12 15:58:06 +00:00
return [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the aggregate node limit" , & location ) }
}
2026-09-29 17:29:01 +00:00
s . accounting . nodes += nodeCount
2026-09-12 15:58:06 +00:00
2026-10-15 15:33:40 +00:00
var body * document . Node
var declarations [ ] importDeclaration
var diagnostics [ ] Diagnostic
2026-10-16 15:48:36 +00:00
var processing * sourceVersionProcessingResult
2026-10-15 15:33:40 +00:00
if s . versionState != nil && s . versionState . mode != sourceVersionModeDefault {
processed , failures := s . versionState . process ( root , sourceVersionContribution , destination , sourceID )
if len ( failures ) != 0 {
return failures
}
2026-10-16 15:48:36 +00:00
processing = & processed
2026-10-15 15:33:40 +00:00
body , declarations = processed . payload , processed . imports
} else {
body , declarations , diagnostics = splitImportDirectives ( root )
if len ( diagnostics ) != 0 {
return diagnostics
}
2026-09-12 15:58:06 +00:00
}
2026-10-14 17:21:07 +00:00
_ , placed , err := placeDocument ( body , destination , body . Location , maxDocumentDepth , maxDocumentNodes )
2026-10-14 13:19:31 +00:00
if err != nil {
return [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the source depth or node limit after destination placement" , & location ) }
}
2026-10-14 17:21:07 +00:00
placementNodes := destinationWrapperNodeCount ( destination )
2026-10-14 13:31:32 +00:00
if s . accounting . nodes + placementNodes > s . limits . nodes {
return [ ] Diagnostic { importDiagnostic ( CodeImportLimitExceeded , "configuration exceeds the aggregate node limit" , & location ) }
}
s . accounting . nodes += placementNodes
2026-10-15 15:33:40 +00:00
protectionRule := s . rule
if s . versionState != nil && s . versionState . effectiveRule != nil {
protectionRule = s . versionState . effectiveRule
}
if versionNode := fragmentVersionClaim ( placed , protectionRule ) ; versionNode != nil {
2026-09-12 15:58:06 +00:00
versionLocation := publicLocation ( versionNode . Location )
return [ ] Diagnostic { {
2026-10-15 15:33:40 +00:00
Code : CodeImportVersion , Severity : SeverityError , Path : protectionRule . Path . clone ( ) ,
2026-09-12 15:58:06 +00:00
Message : "imported fragments cannot contribute to the schema-version path" , Primary : & versionLocation ,
} }
}
route := importRoute { canonical : canonical , locations : currentRoute . locations }
2026-10-16 15:48:36 +00:00
s . scope . contributions = append ( s . scope . contributions , acquiredContribution {
source : cloneSourceInfo ( info ) , payload : body , document : placed ,
destination : destination . clone ( ) , relativeDestination : relativeDestination . clone ( ) , processing : processing ,
} )
2026-09-29 17:29:01 +00:00
s . active [ identity ] = len ( s . stack )
s . visited [ identity ] = route
2026-09-12 15:58:06 +00:00
s . stack = append ( s . stack , route )
2026-10-14 17:21:07 +00:00
diagnostics = s . walkDeclarations ( s . access . dir ( canonical ) , declarations , depth , destination )
2026-09-29 17:29:01 +00:00
delete ( s . active , identity )
2026-09-12 15:58:06 +00:00
s . stack = s . stack [ : len ( s . stack ) - 1 ]
return diagnostics
}
2026-09-29 17:29:01 +00:00
func ( s * importTraversal ) readImport ( name , sourceID string , optional bool ) ( [ ] byte , bool , * Diagnostic ) {
remaining := s . limits . bytes - s . accounting . bytes
2026-09-12 15:58:06 +00:00
limit := min ( int64 ( maxSourceBytes ) , remaining )
if limit < 0 {
limit = 0
}
2026-09-29 17:29:01 +00:00
data , failure := readRegularFile ( name , s . access . stat , s . access . open , limit )
2026-09-12 15:58:06 +00:00
if failure != nil {
if optional && failure . missingTarget {
return nil , true , nil
}
value := sourceReadDiagnostic ( failure , sourceID , CodeImportLimitExceeded , "configuration exceeds a source or aggregate byte limit" )
return nil , false , & value
}
2026-09-29 17:29:01 +00:00
s . accounting . bytes += int64 ( len ( data ) )
2026-09-12 15:58:06 +00:00
return data , false , nil
}
func ( s * importTraversal ) currentRoute ( location Location ) importRoute {
locations := make ( [ ] Location , 0 , len ( s . stack ) + 1 )
for _ , route := range s . stack {
if len ( route . locations ) != 0 {
locations = append ( locations , route . locations [ len ( route . locations ) - 1 ] )
}
}
locations = append ( locations , location )
return importRoute { locations : locations }
}
func sourceInfos ( source * SourceInfo ) [ ] SourceInfo {
if source == nil {
return nil
}
return [ ] SourceInfo { cloneSourceInfo ( * source ) }
}
func importDiagnostic ( code Code , message string , location * Location ) Diagnostic {
return Diagnostic { Code : code , Severity : SeverityError , Message : message , Primary : cloneLocation ( location ) }
}
func importCycleDiagnostic ( target string , location Location , cycle [ ] importRoute ) Diagnostic {
paths := make ( [ ] string , 0 , len ( cycle ) + 1 )
related := make ( [ ] Location , 0 , len ( cycle ) )
for _ , route := range cycle {
paths = append ( paths , route . canonical )
if len ( route . locations ) != 0 {
related = append ( related , route . locations [ len ( route . locations ) - 1 ] )
}
}
paths = append ( paths , target )
return Diagnostic {
Code : CodeImportCycle , Severity : SeverityError ,
Message : "import cycle detected: " + strings . Join ( paths , " -> " ) ,
Primary : & location , Related : related ,
}
}
func importRepeatedDiagnostic ( location Location , previous , current importRoute ) Diagnostic {
related := append ( [ ] Location ( nil ) , previous . locations ... )
if len ( current . locations ) > 1 {
related = append ( related , current . locations [ : len ( current . locations ) - 1 ] ... )
}
return Diagnostic {
Code : CodeImportRepeated , Severity : SeverityError ,
Message : "configuration source is included by more than one import route" ,
Primary : & location , Related : related ,
}
}