Skip to content

Commit 2e5e1d8

Browse files
author
ci-bot
committed
parser works
1 parent aad154c commit 2e5e1d8

File tree

6 files changed

+476
-27
lines changed

6 files changed

+476
-27
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,11 @@ export class Compiler {
165165
this.state.compileJSON = (source: SourceWithTarget) => {
166166
const missingInputs: string[] = []
167167
const missingInputsCallback = (path: string) => {
168+
console.log(`[Compiler] 🚨 MISSING IMPORT DETECTED: "${path}"`)
169+
console.log(`[Compiler] ⛔ Stopping compilation at first missing import for debugging`)
168170
missingInputs.push(path)
169-
return { error: 'Deferred import' }
171+
// Instead of deferring, throw an error to stop compilation immediately
172+
throw new Error(`Missing import: ${path}`)
170173
}
171174
let result: CompilationResult = {}
172175
let input = ""
@@ -267,8 +270,11 @@ export class Compiler {
267270
this.state.compileJSON = (source: SourceWithTarget) => {
268271
const missingInputs: string[] = []
269272
const missingInputsCallback = (path: string) => {
273+
console.log(`[Compiler] 🚨 MISSING IMPORT DETECTED: "${path}"`)
274+
console.log(`[Compiler] ⛔ Stopping compilation at first missing import for debugging`)
270275
missingInputs.push(path)
271-
return { error: 'Deferred import' }
276+
// Instead of deferring, throw an error to stop compilation immediately
277+
throw new Error(`Missing import: ${path}`)
272278
}
273279
let result: CompilationResult = {}
274280
let input = ""
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Example usage of DependencyResolver
3+
*
4+
* This shows how to use the new pre-compilation dependency tree builder
5+
* to resolve imports with full context awareness.
6+
*/
7+
8+
import { DependencyResolver } from './dependency-resolver'
9+
10+
// Example usage in a compilation flow:
11+
export async function exampleUsage(pluginApi: any, contractFile: string) {
12+
console.log('🎯 Starting context-aware dependency resolution...')
13+
14+
// 1. Create a DependencyResolver for the entry file
15+
const depResolver = new DependencyResolver(pluginApi, contractFile)
16+
17+
// 2. Build the complete dependency tree
18+
// This will recursively resolve all imports, tracking which file requests which dependency
19+
const sourceBundle = await depResolver.buildDependencyTree(contractFile)
20+
21+
// 3. Get the complete source bundle for compilation
22+
const compilerInput = depResolver.toCompilerInput()
23+
24+
console.log(`✅ Built source bundle with ${sourceBundle.size} files`)
25+
26+
// 4. Optional: Inspect the import graph
27+
const importGraph = depResolver.getImportGraph()
28+
console.log('📊 Import graph:')
29+
importGraph.forEach((imports, file) => {
30+
console.log(` ${file} imports:`)
31+
imports.forEach(imp => console.log(` - ${imp}`))
32+
})
33+
34+
// 5. Optional: Check package contexts
35+
for (const [file] of sourceBundle) {
36+
const context = depResolver.getPackageContext(file)
37+
if (context) {
38+
console.log(`📦 ${file} belongs to ${context}`)
39+
}
40+
}
41+
42+
// 6. Pass the source bundle to the Solidity compiler
43+
// const compilerOutput = await compile(compilerInput, ...)
44+
45+
return compilerInput
46+
}
47+
48+
/**
49+
* Example scenario: Resolving contracts from multiple parent packages
50+
*
51+
* File structure:
52+
*
53+
* MyContract.sol
54+
* ├─ import "@chainlink/contracts-ccip@1.6.1/src/v0.8/ccip/Router.sol"
55+
* │ └─ import "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/token/ERC20/IERC20.sol"
56+
* │ (This should resolve using contracts-ccip@1.6.1's package.json, which specifies contracts@1.4.0)
57+
* │
58+
* └─ import "@chainlink/contracts-ccip@1.6.2/src/v0.8/ccip/libraries/Client.sol"
59+
* └─ import "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"
60+
* (This should resolve using contracts-ccip@1.6.2's package.json, which specifies contracts@1.5.0)
61+
*
62+
* With the old approach (compiler's missing imports callback):
63+
* - ❌ We don't know which file requested @chainlink/contracts
64+
* - ❌ We can't determine if it should be 1.4.0 or 1.5.0
65+
* - ❌ LIFO approach picks the most recent parent (might be wrong!)
66+
*
67+
* With the new approach (DependencyResolver):
68+
* - ✅ We know Router.sol (from contracts-ccip@1.6.1) requests IERC20.sol
69+
* - ✅ We check contracts-ccip@1.6.1's package.json → contracts@1.4.0
70+
* - ✅ We resolve to @chainlink/contracts@1.4.0/...
71+
* - ✅ Similarly for Client.sol → contracts@1.5.0
72+
* - ✅ Both versions coexist peacefully (different files, no conflict)
73+
*/
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
'use strict'
2+
3+
import { Plugin } from '@remixproject/engine'
4+
import { ImportResolver } from './import-resolver'
5+
6+
/**
7+
* Pre-compilation dependency tree builder
8+
*
9+
* This class manually walks the Solidity import graph BEFORE compilation,
10+
* tracking which file requests which import. This enables accurate resolution
11+
* of dependencies even when multiple versions of packages are used.
12+
*
13+
* Key difference from compiler's missing imports callback:
14+
* - We know the REQUESTING file for each import
15+
* - We can resolve based on that file's package context
16+
* - We build the complete source bundle before compiling
17+
*/
18+
export class DependencyResolver {
19+
private pluginApi: Plugin
20+
private resolver: ImportResolver
21+
private sourceFiles: Map<string, string> = new Map() // resolved path -> content
22+
private processedFiles: Set<string> = new Set() // Track already processed files
23+
private importGraph: Map<string, Set<string>> = new Map() // file -> files it imports
24+
private fileToPackageContext: Map<string, string> = new Map() // file -> package@version it belongs to
25+
26+
constructor(pluginApi: Plugin, targetFile: string) {
27+
this.pluginApi = pluginApi
28+
this.resolver = new ImportResolver(pluginApi, targetFile)
29+
}
30+
31+
/**
32+
* Build complete dependency tree starting from entry file
33+
* Returns a map of resolved paths to their contents
34+
*/
35+
public async buildDependencyTree(entryFile: string): Promise<Map<string, string>> {
36+
console.log(`[DependencyResolver] 🌳 Building dependency tree from: ${entryFile}`)
37+
38+
this.sourceFiles.clear()
39+
this.processedFiles.clear()
40+
this.importGraph.clear()
41+
this.fileToPackageContext.clear()
42+
43+
// Start recursive import resolution
44+
await this.processFile(entryFile, null)
45+
46+
console.log(`[DependencyResolver] ✅ Built source bundle with ${this.sourceFiles.size} files`)
47+
48+
return this.sourceFiles
49+
}
50+
51+
/**
52+
* Check if a path is a local file (not an npm package)
53+
*/
54+
private isLocalFile(path: string): boolean {
55+
// Local files typically:
56+
// - End with .sol
57+
// - Don't start with @ or contain package-like structure
58+
// - Are relative paths or simple filenames
59+
// - But NOT relative paths within npm packages (those should be resolved via ImportResolver)
60+
return path.endsWith('.sol') && !path.includes('@') && !path.includes('node_modules') && !path.startsWith('../') && !path.startsWith('./')
61+
}
62+
63+
/**
64+
* Resolve a relative import path against the current file
65+
* E.g., if currentFile is "@chainlink/contracts-ccip@1.6.1/contracts/applications/CCIPClientExample.sol"
66+
* and importPath is "../libraries/Client.sol",
67+
* result should be "@chainlink/contracts-ccip@1.6.1/contracts/libraries/Client.sol"
68+
*/
69+
private resolveRelativeImport(currentFile: string, importPath: string): string {
70+
if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
71+
return importPath // Not a relative path
72+
}
73+
74+
// Get the directory of the current file
75+
const currentDir = currentFile.substring(0, currentFile.lastIndexOf('/'))
76+
77+
// Split paths into parts
78+
const currentParts = currentDir.split('/')
79+
const importParts = importPath.split('/')
80+
81+
// Process the relative path
82+
for (const part of importParts) {
83+
if (part === '..') {
84+
currentParts.pop() // Go up one directory
85+
} else if (part === '.') {
86+
// Stay in current directory, do nothing
87+
} else {
88+
currentParts.push(part)
89+
}
90+
}
91+
92+
const resolvedPath = currentParts.join('/')
93+
console.log(`[DependencyResolver] 🔗 Resolved relative import: ${importPath}${resolvedPath}`)
94+
return resolvedPath
95+
}
96+
97+
/**
98+
* Process a single file: fetch content, extract imports, resolve dependencies
99+
*/
100+
private async processFile(
101+
importPath: string,
102+
requestingFile: string | null,
103+
packageContext?: string
104+
): Promise<void> {
105+
// Avoid processing the same file twice
106+
if (this.processedFiles.has(importPath)) {
107+
console.log(`[DependencyResolver] ⏭️ Already processed: ${importPath}`)
108+
return
109+
}
110+
111+
console.log(`[DependencyResolver] 📄 Processing: ${importPath}`)
112+
console.log(`[DependencyResolver] 📍 Requested by: ${requestingFile || 'entry point'}`)
113+
114+
if (packageContext) {
115+
console.log(`[DependencyResolver] 📦 Package context: ${packageContext}`)
116+
this.fileToPackageContext.set(importPath, packageContext)
117+
118+
// Tell the resolver about this context so it can make context-aware decisions
119+
this.resolver.setPackageContext(packageContext)
120+
}
121+
122+
this.processedFiles.add(importPath)
123+
124+
try {
125+
let content: string
126+
127+
// Handle local files differently from npm packages
128+
if (this.isLocalFile(importPath)) {
129+
console.log(`[DependencyResolver] 📁 Local file detected, reading directly`, importPath)
130+
// For local files, read directly from file system
131+
content = await this.pluginApi.call('fileManager', 'readFile', importPath)
132+
} else {
133+
// For npm packages, use the resolver
134+
content = await this.resolver.resolveAndSave(importPath, undefined, false)
135+
}
136+
137+
if (!content) {
138+
console.log(`[DependencyResolver] ⚠️ Failed to resolve: ${importPath}`)
139+
return
140+
}
141+
142+
// Store the resolved content using the original import path as key
143+
// The compiler expects source keys to match import statements exactly
144+
const resolvedPath = this.isLocalFile(importPath) ? importPath : this.getResolvedPath(importPath)
145+
this.sourceFiles.set(importPath, content)
146+
147+
// If this is a versioned path (like @package@1.5.0/...) but the original import
148+
// was unversioned, also store under the unversioned path for compiler compatibility
149+
if (!this.isLocalFile(importPath) && importPath.includes('@') && importPath.match(/@[^/]+@\d+\.\d+\.\d+\//)) {
150+
const unversionedPath = importPath.replace(/@([^@/]+(?:\/[^@/]+)?)@\d+\.\d+\.\d+\//, '@$1/')
151+
this.sourceFiles.set(unversionedPath, content)
152+
console.log(`[DependencyResolver] 🔄 Also stored under unversioned path: ${unversionedPath}`)
153+
}
154+
155+
156+
157+
// Determine package context for this file (only for npm packages)
158+
if (!this.isLocalFile(importPath)) {
159+
const filePackageContext = this.extractPackageContext(importPath)
160+
if (filePackageContext) {
161+
this.fileToPackageContext.set(resolvedPath, filePackageContext)
162+
console.log(`[DependencyResolver] 📦 File belongs to: ${filePackageContext}`)
163+
}
164+
}
165+
166+
// Extract imports from this file
167+
const imports = this.extractImports(content)
168+
169+
if (imports.length > 0) {
170+
console.log(`[DependencyResolver] 🔗 Found ${imports.length} imports`)
171+
this.importGraph.set(resolvedPath, new Set(imports))
172+
173+
// Determine the package context to pass to child imports
174+
const currentFilePackageContext = this.isLocalFile(importPath) ? null : this.extractPackageContext(importPath)
175+
176+
// Recursively process each import
177+
for (const importedPath of imports) {
178+
console.log(`[DependencyResolver] ➡️ Processing import: ${importedPath}`)
179+
180+
// Resolve relative imports against the original import path (not the resolved path)
181+
// This ensures the resolved import matches what the compiler expects
182+
let resolvedImportPath = importedPath
183+
if (importedPath.startsWith('./') || importedPath.startsWith('../')) {
184+
resolvedImportPath = this.resolveRelativeImport(importPath, importedPath)
185+
console.log(`[DependencyResolver] 🔗 Resolving import via ImportResolver: "${resolvedImportPath}"`)
186+
}
187+
188+
await this.processFile(resolvedImportPath, resolvedPath, currentFilePackageContext)
189+
}
190+
}
191+
} catch (err) {
192+
console.log(`[DependencyResolver] ❌ Error processing ${importPath}:`, err)
193+
}
194+
}
195+
196+
/**
197+
* Extract import statements from Solidity source code
198+
*/
199+
private extractImports(content: string): string[] {
200+
const imports: string[] = []
201+
202+
// Match: import "path/to/file.sol";
203+
// Match: import 'path/to/file.sol';
204+
// Match: import {Symbol} from "path/to/file.sol";
205+
// Match: import * as Name from "path/to/file.sol";
206+
const importRegex = /import\s+(?:{[^}]*}\s+from\s+)?(?:\*\s+as\s+\w+\s+from\s+)?["']([^"']+)["']/g
207+
208+
let match
209+
while ((match = importRegex.exec(content)) !== null) {
210+
const importPath = match[1]
211+
if (importPath) {
212+
imports.push(importPath)
213+
}
214+
}
215+
216+
return imports
217+
}
218+
219+
/**
220+
* Extract package context from a file path
221+
* E.g., "@openzeppelin/contracts@4.8.0/token/ERC20/IERC20.sol" -> "@openzeppelin/contracts@4.8.0"
222+
*/
223+
private extractPackageContext(path: string): string | null {
224+
// Match: @scope/package@version or package@version at start of path
225+
const scopedMatch = path.match(/^(@[^/]+\/[^/@]+)@([^/]+)/)
226+
if (scopedMatch) {
227+
return `${scopedMatch[1]}@${scopedMatch[2]}`
228+
}
229+
230+
const regularMatch = path.match(/^([^/@]+)@([^/]+)/)
231+
if (regularMatch) {
232+
return `${regularMatch[1]}@${regularMatch[2]}`
233+
}
234+
235+
return null
236+
}
237+
238+
/**
239+
* Get the resolved path for a file (what the compiler will see)
240+
*/
241+
private getResolvedPath(importPath: string): string {
242+
// Get the actual resolved path from the ImportResolver's resolutions
243+
const resolved = this.resolver.getResolution(importPath)
244+
return resolved || importPath
245+
}
246+
247+
/**
248+
* Get the complete source bundle as a map
249+
*/
250+
public getSourceBundle(): Map<string, string> {
251+
return this.sourceFiles
252+
}
253+
254+
/**
255+
* Get the import graph (which files import which)
256+
*/
257+
public getImportGraph(): Map<string, Set<string>> {
258+
return this.importGraph
259+
}
260+
261+
/**
262+
* Get the package context for a file
263+
*/
264+
public getPackageContext(filePath: string): string | null {
265+
return this.fileToPackageContext.get(filePath) || null
266+
}
267+
268+
/**
269+
* Convert source bundle to Solidity compiler input format
270+
*/
271+
public toCompilerInput(): { [fileName: string]: { content: string } } {
272+
const sources: { [fileName: string]: { content: string } } = {}
273+
274+
for (const [path, content] of this.sourceFiles.entries()) {
275+
sources[path] = { content }
276+
}
277+
278+
return sources
279+
}
280+
}

0 commit comments

Comments
 (0)