GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
110 / 115
95.7%
Branches:
49 / 60
81.7%
Functions:
4 / 4
100.0%

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
2/2
✓ Branch 0 taken 1539 times.
✓ Branch 1 taken 3951 times.
5490 foreach IVisitable* attr : node.modAttrs {
35 1539 this.visit(attr);
36 1539 }
37
38 // Visit all import defs
39
2/2
✓ Branch 0 taken 2518 times.
✓ Branch 1 taken 3951 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2518 times.
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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2517 times.
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
2/2
✓ Branch 0 taken 2383 times.
✓ Branch 1 taken 134 times.
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
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 130 times.
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
4/4
✓ Branch 0 taken 2513 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2513 times.
2517 if osArchPath.exists() && !ownFilePath.equivalent(osArchPath) {
106 // file_os_arch.spice is first choice
107
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 importPath = osArchPath;
108
4/4
✓ Branch 0 taken 2471 times.
✓ Branch 1 taken 42 times.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 2472 times.
2517 } else if osPath.exists() && !ownFilePath.equivalent(osPath) {
109 // file_os.spice is second choice
110
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 importPath = osPath;
111
4/4
✓ Branch 0 taken 2471 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 2471 times.
2513 } else if archPath.exists() && !ownFilePath.equivalent(archPath) {
112 // file_arch.spice is third choice
113
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 importPath = archPath;
114
2/2
✓ Branch 0 taken 2468 times.
✓ Branch 1 taken 3 times.
2472 } else if defaultPath.exists() {
115 // file.spice is fourth choice
116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2468 times.
2468 importPath = defaultPath;
117 2468 } else {
118 3 const bool obfuscate = this.cliOptions.comparableOutput;
119
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const String osArchPathObfuscated = obfuscate ? osArchPath.getFileName() : String(osArchPath.toGenericString());
120
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const String osPathObfuscated = obfuscate ? osPath.getFileName() : String(osPath.toGenericString());
121
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const String archPathObfuscated = obfuscate ? archPath.getFileName() : String(archPath.toGenericString());
122
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
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
3/4
✓ Branch 0 taken 2382 times.
✓ Branch 1 taken 132 times.
✓ Branch 2 taken 2514 times.
✗ Branch 3 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2514 times.
2514 const bool isContained = allowedRoot == canonicalImportPath || canonicalImportPathStr.startsWith(allowedRootPrefix.getRaw());
149
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2513 times.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2511 times.
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
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 1491 times.
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
2/2
✓ Branch 0 taken 1535 times.
✓ Branch 1 taken 4 times.
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
2/2
✓ Branch 0 taken 1535 times.
✓ Branch 1 taken 4 times.
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
2/2
✓ Branch 0 taken 1534 times.
✓ Branch 1 taken 5 times.
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