importcollector/import-collector.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/type/any"; | ||
| 5 | import "std/data/vector"; | ||
| 6 | import "std/io/filepath"; | ||
| 7 | |||
| 8 | // Own imports | ||
| 9 | import "@/compiler-pass"; | ||
| 10 | import "@/ast/ast-nodes"; | ||
| 11 | import "@/ast/attributes"; | ||
| 12 | import "@/ast/abstract-ast-visitor-intf"; | ||
| 13 | import "@/exception/error-manager"; | ||
| 14 | import "@/exception/semantic-error"; | ||
| 15 | import "@/global/global-resource-manager"; | ||
| 16 | import "@/source-file"; | ||
| 17 | import "@/symboltablebuilder/symbol-table-entry"; | ||
| 18 | import "@/util/system-util"; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Jobs: | ||
| 22 | * - Visit the import statements of a source file and register the imported files as dependencies to the current one | ||
| 23 | */ | ||
| 24 | public type ImportCollector struct : IAbstractAstVisitor { | ||
| 25 | compose public CompilerPass compilerPass | ||
| 26 | } | ||
| 27 | |||
| 28 | 3951 | public p ImportCollector.ctor(GlobalResourceManager& resourceManager, SourceFile* sourceFile) { | |
| 29 | 3951 | this.compilerPass = CompilerPass(resourceManager, sourceFile); | |
| 30 | 3951 | } | |
| 31 | |||
| 32 | 3951 | public f<Any> ImportCollector.visitEntry(ASTEntryNode* node) { | |
| 33 | // Visit all module attributes | ||
| 34 | 5490 | foreach IVisitable* attr : node.modAttrs { | |
| 35 | 1539 | this.visit(attr); | |
| 36 | 1539 | } | |
| 37 | |||
| 38 | // Visit all import defs | ||
| 39 | 6469 | foreach IVisitable* importDef : node.importDefs { | |
| 40 | 2518 | this.visit(importDef); | |
| 41 | 2518 | } | |
| 42 | |||
| 43 | 3951 | return Any(); | |
| 44 | } | ||
| 45 | |||
| 46 | 2518 | public f<Any> ImportCollector.visitImportDef(ASTImportDefNode* node) { | |
| 47 | // Validate the import path before resolving it against any base directory. Import paths must be relative and | ||
| 48 | // must not contain parent-directory ('..') components. Otherwise a malicious source file could escape the | ||
| 49 | // std/project roots (e.g. 'import "../../../etc/x"' or 'import "std/../../../etc/x"') and read | ||
| 50 | // arbitrary files, or worse, smuggle shell metacharacters into a filename that later ends up in a linker | ||
| 51 | // invocation. | ||
| 52 | 2518 | ASTNode* baseNode = cast<ASTNode*>(node); | |
| 53 | 2518 | ErrorManager& errorManager = this.resourceManager.getErrorManager(); | |
| 54 | 2518 | const FilePath& ownFilePath = this.sourceFile.getFilePath(); | |
| 55 | 2518 | const FilePath importPathRelative = FilePath(node.importPath); | |
| 56 | 2518 | if importPathRelative.hasRootPath() { | |
| 57 | ✗ | errorManager.addHardError(baseNode, SemanticErrorType::INVALID_IMPORT_PATH, "Import paths must be relative, but '" + node.importPath + "' is not"); | |
| 58 | ✗ | return Any(); | |
| 59 | ✗ | } | |
| 60 | 2518 | if importPathRelative.hasParentDirComponent() { | |
| 61 | 1 | errorManager.addHardError(baseNode, SemanticErrorType::INVALID_IMPORT_PATH, "Import paths must not contain parent-directory ('..') components: '" + node.importPath + "'"); | |
| 62 | 2 | return Any(); | |
| 63 | 1 | } | |
| 64 | |||
| 65 | 2517 | const bool isStd = node.importPath.startsWith("std/"); | |
| 66 | // Imports with the '@/' prefix are resolved against the dir of the main source file, independent of the dir of the | ||
| 67 | // importing source file | ||
| 68 | 2517 | const bool isMainRelative = node.importPath.startsWith("@/"); | |
| 69 | |||
| 70 | // Determine the root dir, that the import is resolved against, and the path of the imported file within it | ||
| 71 | 2517 | const String ownFileDir = ownFilePath.getParentDir(); | |
| 72 | 2517 | FilePath rootDir; | |
| 73 | 2517 | String pathInRootDir = node.importPath; | |
| 74 | 2517 | if isStd { // Include source file from standard library | |
| 75 | 2383 | rootDir = getStdDir(); | |
| 76 | 2383 | pathInRootDir = node.importPath.getSubstring(node.importPath.find("std/") + 4l); | |
| 77 | 2517 | } else if isMainRelative { // Include own source file, relative to the main source file | |
| 78 | 4 | rootDir = FilePath(this.cliOptions.mainSourceFile.getParentDir()); | |
| 79 | 4 | pathInRootDir = node.importPath.getSubstring(2ul); | |
| 80 | 4 | } else { // Include own source file, relative to the importing source file | |
| 81 | 130 | rootDir = FilePath(ownFileDir); | |
| 82 | 134 | } | |
| 83 | 2517 | FilePath basePath = rootDir / pathInRootDir; | |
| 84 | |||
| 85 | 2517 | const String osName = this.cliOptions.targetTriple.getOSTypeName(); | |
| 86 | 2517 | const String archName = this.cliOptions.targetTriple.getArchTypeName(); | |
| 87 | |||
| 88 | // basePath never has a '.spice' extension yet at this point (import paths are given without one), so | ||
| 89 | // getFileName() (not getBaseName(), which assumes an extension is present) is the right way to get the | ||
| 90 | // bare stem to build the four candidate filenames from. | ||
| 91 | 2517 | const String stem = basePath.getFileName(); | |
| 92 | 2517 | const String parentDir = basePath.getParentDir(); | |
| 93 | |||
| 94 | // Format: /dir/to/path/file.spice | ||
| 95 | 2517 | const FilePath defaultPath = FilePath(parentDir) / (stem + ".spice"); | |
| 96 | // Format: /dir/to/path/file_x86_64.spice | ||
| 97 | 2517 | const FilePath archPath = FilePath(parentDir) / (stem + "_" + archName + ".spice"); | |
| 98 | // Format: /dir/to/path/file_linux.spice | ||
| 99 | 2517 | const FilePath osPath = FilePath(parentDir) / (stem + "_" + osName + ".spice"); | |
| 100 | // Format: /dir/to/path/file_linux_x86_64.spice | ||
| 101 | 2517 | const FilePath osArchPath = FilePath(parentDir) / (stem + "_" + osName + "_" + archName + ".spice"); | |
| 102 | |||
| 103 | // Check which source file to import | ||
| 104 | 2517 | FilePath importPath; | |
| 105 | 2517 | if osArchPath.exists() && !ownFilePath.equivalent(osArchPath) { | |
| 106 | // file_os_arch.spice is first choice | ||
| 107 | 4 | importPath = osArchPath; | |
| 108 | 2517 | } else if osPath.exists() && !ownFilePath.equivalent(osPath) { | |
| 109 | // file_os.spice is second choice | ||
| 110 | 41 | importPath = osPath; | |
| 111 | 2513 | } else if archPath.exists() && !ownFilePath.equivalent(archPath) { | |
| 112 | // file_arch.spice is third choice | ||
| 113 | 1 | importPath = archPath; | |
| 114 | 2472 | } else if defaultPath.exists() { | |
| 115 | // file.spice is fourth choice | ||
| 116 | 2468 | importPath = defaultPath; | |
| 117 | 2468 | } else { | |
| 118 | 3 | const bool obfuscate = this.cliOptions.comparableOutput; | |
| 119 | 3 | const String osArchPathObfuscated = obfuscate ? osArchPath.getFileName() : String(osArchPath.toGenericString()); | |
| 120 | 3 | const String osPathObfuscated = obfuscate ? osPath.getFileName() : String(osPath.toGenericString()); | |
| 121 | 3 | const String archPathObfuscated = obfuscate ? archPath.getFileName() : String(archPath.toGenericString()); | |
| 122 | 3 | const String defaultPathObfuscated = obfuscate ? defaultPath.getFileName() : String(defaultPath.toGenericString()); | |
| 123 | |||
| 124 | 3 | String errorMessage = "The source file '" + stem + ".spice' was not found\n\n"; | |
| 125 | 3 | errorMessage += "The following paths were checked with descending priority:\n"; | |
| 126 | 3 | errorMessage += "- " + osArchPathObfuscated + "\n"; | |
| 127 | 3 | errorMessage += "- " + osPathObfuscated + "\n"; | |
| 128 | 3 | errorMessage += "- " + archPathObfuscated + "\n"; | |
| 129 | 3 | errorMessage += "- " + defaultPathObfuscated; | |
| 130 | 3 | errorManager.addHardError(baseNode, SemanticErrorType::IMPORTED_FILE_NOT_EXISTING, errorMessage); | |
| 131 | 6 | return Any(); | |
| 132 | 2513 | } | |
| 133 | |||
| 134 | // The lexical '..'-component check above only rejects traversal spelled out in the import path itself. A | ||
| 135 | // same-looking relative import can still resolve outside the intended std/project root if a | ||
| 136 | // directory along the way is a symlink. Canonicalize the resolved file and its intended base directory | ||
| 137 | // (this also resolves symlinks) and reject the import unless the former stays contained in the latter. | ||
| 138 | // A source file given as a bare filename (e.g. 'spice build main.spice') has an empty parent dir; treat that | ||
| 139 | // as the current directory, since that is what the empty path already means to every other join above. | ||
| 140 | 2514 | FilePath intendedRoot = rootDir; | |
| 141 | 2514 | if !isStd && rootDir.isEmpty() { | |
| 142 | ✗ | intendedRoot = FilePath(String(".")); | |
| 143 | ✗ | } | |
| 144 | 2514 | const FilePath allowedRoot = intendedRoot.canonical(); | |
| 145 | 2514 | const FilePath canonicalImportPath = importPath.canonical(); | |
| 146 | 2514 | const String allowedRootPrefix = String(allowedRoot.toGenericString()) + "/"; | |
| 147 | 2514 | const String canonicalImportPathStr = String(canonicalImportPath.toGenericString()); | |
| 148 | 2514 | const bool isContained = allowedRoot == canonicalImportPath || canonicalImportPathStr.startsWith(allowedRootPrefix.getRaw()); | |
| 149 | 2514 | if !isContained { | |
| 150 | 1 | errorManager.addHardError(baseNode, SemanticErrorType::INVALID_IMPORT_PATH, "Import path '" + node.importPath + "' escapes the permitted directory tree"); | |
| 151 | 2 | return Any(); | |
| 152 | 1 | } | |
| 153 | |||
| 154 | // Check if the import already exists | ||
| 155 | 2513 | if this.rootScope.lookupStrict(node.importName) != nil<SymbolTableEntry*> { | |
| 156 | 2 | errorManager.addHardError(baseNode, SemanticErrorType::DUPLICATE_IMPORT_NAME, "Duplicate import '" + node.importName + "'"); | |
| 157 | 4 | return Any(); | |
| 158 | 2 | } | |
| 159 | |||
| 160 | // Create symbol for import | ||
| 161 | 2511 | node.entry = this.rootScope.insert(node.importName, baseNode); | |
| 162 | |||
| 163 | // Create the imported source file | ||
| 164 | 2511 | SourceFile* importedSourceFile = this.resourceManager.createSourceFile(this.sourceFile, node.importName, importPath, isStd); | |
| 165 | // Register it as a dependency to the current source file | ||
| 166 | 2511 | this.sourceFile.addDependency(importedSourceFile, node.importName); | |
| 167 | |||
| 168 | 5022 | return Any(); | |
| 169 | 2518 | } | |
| 170 | |||
| 171 | 1539 | public f<Any> ImportCollector.visitModAttr(ASTModAttrNode* node) { | |
| 172 | // !!! Only bool attributes allowed here, due to missing attribute value checks being executed in a later stage !!! | ||
| 173 | |||
| 174 | // core.compiler.keep-on-name-collision | ||
| 175 | 1539 | if node.attrLst.hasAttr(String(ATTR_CORE_COMPILER_KEEP_ON_NAME_COLLISION)) { | |
| 176 | 1491 | const CompileTimeValue keepOnCollisionAttrValue = node.attrLst.getAttrValueByName(String(ATTR_CORE_COMPILER_KEEP_ON_NAME_COLLISION)); | |
| 177 | 1491 | const bool keepOnCollision = keepOnCollisionAttrValue.getBoolValue(); | |
| 178 | 1491 | this.sourceFile.setAlwaysKeepSymbolsOnNameCollision(keepOnCollision); | |
| 179 | 1491 | } | |
| 180 | |||
| 181 | // core.compiler.warnings.ignore | ||
| 182 | 1539 | if node.attrLst.hasAttr(String(ATTR_CORE_COMPILER_WARNINGS_IGNORE)) { | |
| 183 | 4 | const CompileTimeValue ignoreWarningsAttrValue = node.attrLst.getAttrValueByName(String(ATTR_CORE_COMPILER_WARNINGS_IGNORE)); | |
| 184 | 4 | const bool ignoreWarnings = ignoreWarningsAttrValue.getBoolValue(); | |
| 185 | 4 | this.sourceFile.setIgnoreWarnings(ignoreWarnings); | |
| 186 | 4 | } | |
| 187 | |||
| 188 | // core.compiler.explicitErrorHandling | ||
| 189 | 1539 | if node.attrLst.hasAttr(String(ATTR_CORE_COMPILER_EXPLICIT_ERROR_HANDLING)) { | |
| 190 | 4 | const CompileTimeValue explicitErrorHandlingAttrValue = node.attrLst.getAttrValueByName(String(ATTR_CORE_COMPILER_EXPLICIT_ERROR_HANDLING)); | |
| 191 | 4 | const bool explicitErrorHandling = explicitErrorHandlingAttrValue.getBoolValue(); | |
| 192 | 4 | this.sourceFile.setExplicitErrorHandling(explicitErrorHandling); | |
| 193 | 4 | } | |
| 194 | |||
| 195 | // core.compiler.errorReturnTracing | ||
| 196 | 1539 | if node.attrLst.hasAttr(String(ATTR_CORE_COMPILER_ERROR_RETURN_TRACING)) { | |
| 197 | 5 | const CompileTimeValue errorReturnTracingAttrValue = node.attrLst.getAttrValueByName(String(ATTR_CORE_COMPILER_ERROR_RETURN_TRACING)); | |
| 198 | 5 | const bool errorReturnTracing = errorReturnTracingAttrValue.getBoolValue(); | |
| 199 | 5 | this.sourceFile.setErrorReturnTracing(errorReturnTracing); | |
| 200 | 5 | } | |
| 201 | |||
| 202 | 1539 | return Any(); | |
| 203 | } | ||
| 204 |