Windows absolute paths in standard JSON break import resolution and library linking
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Description
When `solc --standard-json` receives source paths containing backslashes (e.g., absolute Windows paths like `C:\temp\debug\contract.sol`), **relative** import resolution produces forward slashed paths that don't match the original source paths.
The import is treated as missing, loaded from disk as a separate (duplicate) source unit, and depending on the import pattern this causes one of three failures on Windows:
1. **Linker symbol mismatch**
* When a contract imports a library with `public`/`external` (*not* `internal`) functions, the linker symbol in the Yul uses the duplicate's source unit name instead of the original, so `settings.libraries` addresses are not linked (the bytecode contains unresolved placeholders).
* This also affects downstream tools that consume the Yul.
2. **`DeclarationError: Identifier already declared`**
* When two files import each other (circular imports), the original source transitively imports a duplicate of itself, causing the same identifiers to be declared twice in the same scope.
3. **`TypeError`**
* When a file imports itself via a module alias (e.g., `import "./self.sol" as Self`), the original source unit and the duplicate loaded from disk each define their own copy of the same type. Since these get treated as distinct types, operations that cross this boundary (e.g., `using { f } for Self.T global`) fail with a type mismatch.
### Source Unit Name Ambiguities
I believe there are ambiguities as to what standard JSON `sources` keys (the source unit names) are allowed and expected, especially when source files use relative imports:
* According to the [import path resolution docs](https://docs.soliditylang.org/en/latest/path-resolution.html#import-callback), a source unit name is an *"opaque and unstructured identifier"*.
* It seems fair to assume that such source unit names can thereby contain backslashes (e.g. Windows paths).
* The docs claim that the relative import algorithm only uses `/` as a separator, but [absolutePath()](https://github.com/argotorg/solidity/blob/64118f21280f0196f491689f0fc93dc5bec20dc1/libsolutil/CommonIO.cpp#L140-L158) in the implementation uses platform-specific separators.
* The docs recommend forward slashed `import` paths, but:
* If using `/` as separator (current docs):
* The relative import resolution will not in this case find the corresponding source unit in the VFS if the source unit name contains backslashes.
* If using platform-specific separators (current code):
* The resolved path will match the source unit name, but `absolutePath()` returns it only after normalizing the return value to forward slashes via [generic_string()](https://github.com/argotorg/solidity/blob/64118f21280f0196f491689f0fc93dc5bec20dc1/libsolutil/CommonIO.cpp#L157).
* In both scenarios (the latter is shown in the reproduction below), the expected source unit is not found in the VFS, causing the Host Filesystem Loader (which handles platform-specific paths) to load it from disk as a separate source unit.
### Unaffected
This issue is not present when using CLI input files (e.g. `solc C:\temp\debug\file1.sol C:\temp\debug\file2.sol`), since it normalizes the input file paths.
### Related
* [#14559](https://github.com/argotorg/solidity/issues/14559)
* [absolutePath()](https://github.com/argotorg/solidity/blob/64118f21280f0196f491689f0fc93dc5bec20dc1/libsolutil/CommonIO.cpp#L140-L158) is involved in both issues.
* For this issue, since `boost::filesystem::path::generic_string()` returns a forward slashed value, it will differ from the backslashed input source paths (source unit names)
## Environment
- Compiler version: 0.8.33 (from [solc-bin](https://github.com/argotorg/solc-bin/tree/gh-pages/windows-amd64))
- Compilation pipeline: IR
- Operating system: Windows
## Steps to Reproduce
These steps have been run on Windows in a bash shell.
**Notes:**
* The Solidity source files themselves still use forward slash relative imports, as recommended by the [docs](https://docs.soliditylang.org/en/latest/path-resolution.html#import-callback).
* To reproduce the errors below, the source files must exist on disk at the paths used as source keys, otherwise the error will instead be `Source not found`.
* In addition to the examples below, a fix for this issue should take into account Windows extended-length paths (using the `\\?\` prefix, e.g. `\\?\C:\temp\debug\library.sol`).
### Prerequisites
```bash
# For formatting JSON input and output.
choco install jq
# For input and output files.
mkdir -p /c/temp/debug
cd /c/temp/debug
```
### 1. Library Linker Symbol Mismatch - One-Directional Import
#### Create a contract calling an `external` library function:
```bash
cat > /c/temp/debug/library.sol << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library L {
function f() external pure returns (uint) { return 1; }
}
EOF
```
```bash
cat > /c/temp/debug/contract.sol << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./library.sol";
contract C {
function g() external pure returns (uint) { return L.f(); }
}
EOF
```
#### Create the standard JSON input file:
```bash
jq -n \
--rawfile contract /c/temp/debug/contract.sol \
--rawfile library /c/temp/debug/library.sol \
'{
language: "Solidity",
sources: {
"C:\\temp\\debug\\contract.sol": { content: $contract },
"C:\\temp\\debug\\library.sol": { content: $library }
},
settings: {
viaIR: true,
libraries: {
"C:\\temp\\debug\\library.sol": {
L: "0x0000000000000000000000000000000000000001"
}
},
outputSelection: {
"*": {
"*": ["irOptimized", "evm.bytecode"]
}
}
}
}' > /c/temp/debug/input-library.json
```
#### Compile:
```bash
solc --standard-json < /c/temp/debug/input-library.json | jq . > /c/temp/debug/output-library.json
```
#### Expected:
* Compiles successfully (no `errors` items with `"severity": "error"`)
* 2 source units
* Yul with linker symbols matching `libraries` keys
* Fully linked bytecode
#### Actual:
3 source units instead of 2:
* The forward slash duplicate (`id: 0`) was loaded from disk by the import callback
* The original backslash entry (`id: 2`) is unused
* The `contracts` output also contains the two different entries of the library
```json
"sources": {
"C:/temp/debug/library.sol": { "id": 0 },
"C:\\temp\\debug\\contract.sol": { "id": 1 },
"C:\\temp\\debug\\library.sol": { "id": 2 }
}
```
Unexpected linker symbol:
* Contract `C`'s Yul references the duplicate's path in the linker symbol
* `linkReferences` also uses the duplicate (bytecode not fully linked)
```yul
linkersymbol("C:/temp/debug/library.sol:L")
```
```json
"linkReferences": {
"C:/temp/debug/library.sol": {
"L": [{ "length": 20, "start": 405 }]
}
}
```
### 2. `DeclarationError` - Circular Imports
#### Create two files that import each other:
```bash
cat > /c/temp/debug/first.sol << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./second.sol";
contract A {}
EOF
```
```bash
cat > /c/temp/debug/second.sol << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./first.sol";
contract B {}
EOF
```
#### Create the standard JSON input file:
```bash
jq -n \
--rawfile first /c/temp/debug/first.sol \
--rawfile second /c/temp/debug/second.sol \
'{
language: "Solidity",
sources: {
"C:\\temp\\debug\\first.sol": { content: $first },
"C:\\temp\\debug\\second.sol": { content: $second }
},
settings: {
viaIR: true,
outputSelection: {
"*": {
"*": ["irOptimized", "evm.bytecode"]
}
}
}
}' > /c/temp/debug/input-circular.json
```
#### Compile:
```bash
solc --standard-json < /c/temp/debug/input-circular.json | jq . > /c/temp/debug/output-circular.json
```
#### Expected:
* Compiles successfully (no `errors` items with `"severity": "error"`) and generates bytecode
* 2 source units
#### Actual:
Compilation fails:
```
DeclarationError: Identifier already declared.
--> C:\temp\debug\first.sol:4:1
|
4 | contract A {}
Note: The previous declaration is here:
--> C:\temp\debug\first.sol:3:1
|
3 | import "./second.sol";
```
### 3. `TypeError` - Self-Import
#### Create a file that imports itself:
```bash
cat > /c/temp/debug/self_import.sol << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./self_import.sol" as Self;
type T is uint;
function f(T x) pure returns (T) { return T.wrap(T.unwrap(x) + 1); }
using { f } for Self.T global;
EOF
```
#### Create the standard JSON input file:
```bash
jq -n \
--rawfile src /c/temp/debug/self_import.sol \
'{
language: "Solidity",
sources: {
"C:\\temp\\debug\\self_import.sol": { content: $src }
},
settings: {
viaIR: true,
outputSelection: {
"*": {
"*": ["irOptimized", "evm.bytecode"]
}
}
}
}' > /c/temp/debug/input-self-import.json
```
#### Compile:
```bash
solc --standard-json < /c/temp/debug/input-self-import.json | jq . > /c/temp/debug/output-self-import.json
```
#### Expected:
* Compiles successfully (no `errors` items with `"severity": "error"`)
* 1 source unit
#### Actual:
Compilation fails:
* `Self.T` comes from the duplicate source unit, so it's a different type than the local `T`.
```
TypeError: Can only use "global" with types defined in the same source unit at file level.
--> C:\temp\debug\self_import.sol:6:1
|
6 | using { f } for Self.T global;
TypeError: The function "f" cannot be attached to the type "T" because the type cannot
be implicitly converted to the first argument of the function ("T").
--> C:\temp\debug\self_import.sol:6:9
|
6 | using { f } for Self.T global;
| ^
```
Contributor guide
Assessment
This issue has not been assessed yet.