Skip to content

Commit d0b30e0

Browse files
author
ci-bot
committed
logging
1 parent d693c76 commit d0b30e0

File tree

11 files changed

+542
-233
lines changed

11 files changed

+542
-233
lines changed

apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ export default class CodeParserCompiler {
125125
return this.plugin.call('contentImport', 'resolveAndSave', url)
126126
.then((result) => cb(null, result))
127127
.catch((error: Error) => cb(error))
128-
}
128+
},
129+
null, // importResolverFactory - not used by SmartCompiler
130+
false // debug - set to false for code-parser to reduce noise
129131
)
130132
this.compiler.event.register('compilationFinished', this.onAstFinished)
131133
}
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Context-Aware Dependency Resolution Strategy
2+
3+
## The Problem
4+
5+
When compiling Solidity contracts with npm package imports, the Solidity compiler provides a "missing imports" callback that only gives us:
6+
- ❌ The missing import path (e.g., `@chainlink/contracts/src/v0.8/token/IERC20.sol`)
7+
- ❌ NO information about which file requested it
8+
9+
This creates ambiguity when:
10+
1. Multiple parent packages depend on different versions of the same package
11+
2. We need to resolve unversioned imports based on their parent's `package.json`
12+
13+
### Real-World Example
14+
15+
```solidity
16+
// MyContract.sol
17+
import "@chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol";
18+
import "@chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol";
19+
```
20+
21+
Where:
22+
- `contracts-ccip@1.6.1/package.json``"@chainlink/contracts": "^1.4.0"`
23+
- `contracts-ccip@1.6.2/package.json``"@chainlink/contracts": "^1.5.0"`
24+
25+
When `Router.sol` imports `@chainlink/contracts/...` (unversioned), should we resolve to 1.4.0 or 1.5.0?
26+
27+
**Old approach:** Use LIFO (most recent parent) → **WRONG!** Might pick the wrong version
28+
**New approach:** Track which file requests what → **CORRECT!** Use the requesting file's package context
29+
30+
---
31+
32+
## The Solution: Pre-Compilation Dependency Tree Builder
33+
34+
Instead of relying on the compiler's missing imports callback, we **build our own dependency tree BEFORE compilation**.
35+
36+
### Architecture
37+
38+
```
39+
┌─────────────────────────────────────────────────────────────────┐
40+
│ DependencyResolver │
41+
│ (Pre-compilation dependency tree builder) │
42+
│ │
43+
│ 1. Start from entry file (e.g., MyContract.sol) │
44+
│ 2. Fetch content │
45+
│ 3. Extract imports using regex │
46+
│ 4. For each import: │
47+
│ a. Track: "File X requests import Y" │
48+
│ b. Determine package context of File X │
49+
│ c. Tell ImportResolver: "Use context of File X" │
50+
│ d. Resolve import Y with full context │
51+
│ e. Recursively process imported file │
52+
│ 5. Build complete source bundle │
53+
│ 6. Pass to Solidity compiler │
54+
└─────────────────────────────────────────────────────────────────┘
55+
56+
┌─────────────────────────────────────────────────────────────────┐
57+
│ ImportResolver │
58+
│ (Context-aware version resolution) │
59+
│ │
60+
│ Priority waterfall: │
61+
│ 1. Workspace resolutions (package.json) │
62+
│ 2. Parent package dependencies ← NOW CONTEXT-AWARE! │
63+
│ 3. Lock files (yarn.lock / package-lock.json) │
64+
│ 4. NPM registry (fallback) │
65+
│ │
66+
│ New method: setPackageContext(packageContext) │
67+
│ - Called by DependencyResolver before each resolution │
68+
│ - Tells resolver: "I'm resolving from within package X@Y" │
69+
│ - findParentPackageContext() uses this for accurate lookup │
70+
└─────────────────────────────────────────────────────────────────┘
71+
```
72+
73+
---
74+
75+
## Key Components
76+
77+
### 1. **DependencyResolver** (NEW!)
78+
`libs/remix-solidity/src/compiler/dependency-resolver.ts`
79+
80+
**Purpose:** Pre-compilation import tree walker
81+
82+
**Responsibilities:**
83+
- Walk the import graph manually (before compilation)
84+
- Track: `File A → imports B` (the missing context!)
85+
- Extract package context from file paths
86+
- Set context before resolving each import
87+
- Build complete source bundle
88+
- Provide compiler-ready input
89+
90+
**Key Methods:**
91+
- `buildDependencyTree(entryFile)` - Main entry point
92+
- `processFile(importPath, requestingFile, packageContext)` - Recursive import processor
93+
- `extractImports(content)` - Regex-based import extraction
94+
- `extractPackageContext(path)` - Extract `package@version` from path
95+
- `toCompilerInput()` - Convert to Solidity compiler format
96+
97+
### 2. **ImportResolver** (ENHANCED!)
98+
`libs/remix-solidity/src/compiler/import-resolver.ts`
99+
100+
**Purpose:** Context-aware version resolution
101+
102+
**New Methods:**
103+
- `setPackageContext(packageContext)` - Set explicit resolution context
104+
- `getResolution(originalImport)` - Get resolved path for import
105+
106+
**Enhanced Methods:**
107+
- `findParentPackageContext()` - Now checks explicit context first
108+
- All existing resolution logic remains unchanged
109+
110+
---
111+
112+
## How It Works: Step-by-Step
113+
114+
### Scenario: Resolving `@chainlink/contracts` from two different parent versions
115+
116+
```
117+
Step 1: DependencyResolver starts with MyContract.sol
118+
└─ Extracts imports:
119+
- "@chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol"
120+
- "@chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol"
121+
122+
Step 2: Process Router.sol
123+
└─ Package context: "@chainlink/contracts-ccip@1.6.1"
124+
└─ Set context: resolver.setPackageContext("@chainlink/contracts-ccip@1.6.1")
125+
└─ Fetch content
126+
└─ Extract imports: "@chainlink/contracts/src/v0.8/token/IERC20.sol"
127+
128+
Step 3: Resolve IERC20.sol (requested by Router.sol)
129+
└─ Current context: "@chainlink/contracts-ccip@1.6.1"
130+
└─ ImportResolver checks parent deps: contracts-ccip@1.6.1 → contracts@1.4.0
131+
└─ Resolves to: "@chainlink/contracts@1.4.0/src/v0.8/token/IERC20.sol" ✅
132+
133+
Step 4: Process Client.sol
134+
└─ Package context: "@chainlink/contracts-ccip@1.6.2"
135+
└─ Set context: resolver.setPackageContext("@chainlink/contracts-ccip@1.6.2")
136+
└─ Fetch content
137+
└─ Extract imports: "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"
138+
139+
Step 5: Resolve OwnerIsCreator.sol (requested by Client.sol)
140+
└─ Current context: "@chainlink/contracts-ccip@1.6.2"
141+
└─ ImportResolver checks parent deps: contracts-ccip@1.6.2 → contracts@1.5.0
142+
└─ Resolves to: "@chainlink/contracts@1.5.0/src/v0.8/shared/access/OwnerIsCreator.sol" ✅
143+
144+
Step 6: Build source bundle
145+
└─ MyContract.sol
146+
└─ @chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol
147+
└─ @chainlink/contracts@1.4.0/src/v0.8/token/IERC20.sol
148+
└─ @chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol
149+
└─ @chainlink/contracts@1.5.0/src/v0.8/shared/access/OwnerIsCreator.sol
150+
151+
Step 7: Pass to Solidity compiler
152+
└─ Compilation succeeds! ✅
153+
└─ No duplicate declarations (different files from different versions)
154+
```
155+
156+
---
157+
158+
## Migration Guide
159+
160+
### Old Approach (Compiler Callback)
161+
```typescript
162+
// Compiler calls missing imports callback
163+
compiler.compile(sources, {
164+
import: async (path: string) => {
165+
// ❌ We don't know which file requested this import!
166+
const content = await importResolver.resolveAndSave(path)
167+
return { contents: content }
168+
}
169+
})
170+
```
171+
172+
### New Approach (Pre-Compilation Builder)
173+
```typescript
174+
import { DependencyResolver } from './dependency-resolver'
175+
176+
// 1. Build dependency tree BEFORE compilation
177+
const depResolver = new DependencyResolver(pluginApi, entryFile)
178+
const sourceBundle = await depResolver.buildDependencyTree(entryFile)
179+
180+
// 2. Get compiler-ready input
181+
const compilerInput = depResolver.toCompilerInput()
182+
183+
// 3. Compile with complete source bundle (no missing imports!)
184+
const output = await compiler.compile({
185+
sources: compilerInput,
186+
settings: { ... }
187+
})
188+
```
189+
190+
---
191+
192+
## Benefits
193+
194+
1.**Context-Aware Resolution**
195+
- Know exactly which file requests each import
196+
- Use the requesting file's package context
197+
- Accurate parent dependency resolution
198+
199+
2.**Multi-Version Support**
200+
- Different parent packages can use different versions of the same dependency
201+
- No conflicts as long as they import different files
202+
- Compiler receives the correct version for each import
203+
204+
3.**Better Error Messages**
205+
- Can warn when multiple parent packages conflict
206+
- Show user exactly which file needs which version
207+
- Suggest actionable solutions
208+
209+
4.**Predictable Behavior**
210+
- No LIFO heuristics (which might be wrong)
211+
- Deterministic resolution based on actual package dependencies
212+
- Same result every time
213+
214+
5.**Full Import Graph Visibility**
215+
- Track complete dependency tree
216+
- Debug import issues easily
217+
- Understand what the compiler will receive
218+
219+
---
220+
221+
## Edge Cases Handled
222+
223+
### Case 1: Two parent packages, same child dependency, different versions
224+
**Solution:** Context-aware resolution uses the correct parent's package.json
225+
226+
### Case 2: Circular imports
227+
**Solution:** `processedFiles` Set prevents infinite loops
228+
229+
### Case 3: Missing files
230+
**Solution:** Graceful error handling, continues processing other imports
231+
232+
### Case 4: Workspace overrides
233+
**Solution:** Priority 1 in resolution waterfall (overrides everything)
234+
235+
---
236+
237+
## Future Enhancements
238+
239+
1. **Parallel Processing**
240+
- Process independent imports concurrently
241+
- Faster build times for large projects
242+
243+
2. **Caching**
244+
- Cache processed files across compilations
245+
- Only re-process changed files
246+
247+
3. **Conflict Detection**
248+
- Warn when same file imported from multiple versions
249+
- Suggest refactoring strategies
250+
251+
4. **Visualization**
252+
- Generate import graph visualizations
253+
- Show dependency tree in IDE
254+
255+
---
256+
257+
## Testing
258+
259+
See `importResolver.test.ts` for test cases including:
260+
- ✅ Basic resolution
261+
- ✅ Explicit versioned imports
262+
- ✅ Parent dependency resolution
263+
- ✅ Chainlink CCIP scenario (multi-parent)
264+
- ✅ Workspace resolutions
265+
- ✅ Lock file versions
266+
267+
---
268+
269+
## Files Changed
270+
271+
1. **NEW:** `libs/remix-solidity/src/compiler/dependency-resolver.ts`
272+
- Pre-compilation dependency tree builder
273+
274+
2. **ENHANCED:** `libs/remix-solidity/src/compiler/import-resolver.ts`
275+
- Added `setPackageContext()` method
276+
- Enhanced `findParentPackageContext()` to check explicit context
277+
- Added `getResolution()` method
278+
- Added conflict warning for multi-parent dependencies
279+
280+
3. **NEW:** `libs/remix-solidity/src/compiler/dependency-resolver.example.ts`
281+
- Example usage documentation
282+
283+
---
284+
285+
## Summary
286+
287+
The new **DependencyResolver** gives us the missing piece: **which file requests which import**.
288+
289+
By building the dependency tree ourselves (instead of relying on the compiler), we can:
290+
- Track the full import graph
291+
- Resolve imports with complete context
292+
- Support multiple versions of parent packages
293+
- Provide better error messages
294+
- Ensure deterministic, predictable behavior
295+
296+
This is a **game-changer** for complex dependency scenarios! 🎉

libs/remix-solidity/src/compiler/compiler-helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { CompilerAbstract } from './compiler-abstract'
44
import { Compiler } from './compiler'
55
import type { CompilerSettings, Language, Source } from './types'
66

7-
export const compile = (compilationTargets: Source, settings: CompilerSettings, language: Language, version: string, contentResolverCallback): Promise<CompilerAbstract> => {
7+
export const compile = (compilationTargets: Source, settings: CompilerSettings, language: Language, version: string, contentResolverCallback, debug: boolean = false): Promise<CompilerAbstract> => {
88
return new Promise((resolve, reject) => {
9-
const compiler = new Compiler(contentResolverCallback)
9+
const compiler = new Compiler(contentResolverCallback, null, debug)
1010
compiler.set('evmVersion', settings?.evmVersion)
1111
compiler.set('optimize', settings?.optimizer?.enabled)
1212
compiler.set('language', language)

0 commit comments

Comments
 (0)