1111 * 3. module/ without __init__.py (namespace package)
1212 */
1313
14- import * as vscode from "vscode"
15- import { analyzeFile } from "./analyzer"
14+ import type { FileSystem } from "./filesystem"
1615import type { ImportInfo } from "./internal"
17- import type { Parser } from "./parser"
1816import { uriDirname } from "./pathUtils"
1917
2018/**
@@ -23,20 +21,14 @@ import { uriDirname } from "./pathUtils"
2321 */
2422const fileExistsCache = new Map < string , boolean > ( )
2523
26- async function cachedExists ( uri : vscode . Uri ) : Promise < boolean > {
27- const key = uri . toString ( )
28- const cached = fileExistsCache . get ( key )
24+ async function cachedExists ( uri : string , fs : FileSystem ) : Promise < boolean > {
25+ const cached = fileExistsCache . get ( uri )
2926 if ( cached !== undefined ) {
3027 return cached
3128 }
32- try {
33- await vscode . workspace . fs . stat ( uri )
34- fileExistsCache . set ( key , true )
35- return true
36- } catch {
37- fileExistsCache . set ( key , false )
38- return false
39- }
29+ const exists = await fs . exists ( uri )
30+ fileExistsCache . set ( uri , exists )
31+ return exists
4032}
4133
4234/** Clears the file existence cache. Call when files may have changed. */
@@ -50,16 +42,17 @@ export function clearImportCache(): void {
5042 * (matching Python's import resolution order).
5143 */
5244async function resolvePythonModule (
53- baseUri : vscode . Uri ,
54- ) : Promise < vscode . Uri | null > {
45+ baseUri : string ,
46+ fs : FileSystem ,
47+ ) : Promise < string | null > {
5548 // Try module.py
56- const pyUri = baseUri . with ( { path : `${ baseUri . path } .py` } )
57- if ( await cachedExists ( pyUri ) ) {
49+ const pyUri = `${ baseUri } .py`
50+ if ( await cachedExists ( pyUri , fs ) ) {
5851 return pyUri
5952 }
6053 // Try module/__init__.py
61- const initUri = vscode . Uri . joinPath ( baseUri , "__init__.py" )
62- if ( await cachedExists ( initUri ) ) {
54+ const initUri = fs . joinPath ( baseUri , "__init__.py" )
55+ if ( await cachedExists ( initUri , fs ) ) {
6356 return initUri
6457 }
6558 return null
@@ -87,10 +80,11 @@ function findImportByExportedName(
8780 */
8881function modulePathToDir (
8982 importInfo : Pick < ImportInfo , "modulePath" | "isRelative" | "relativeDots" > ,
90- currentFileUri : vscode . Uri ,
91- projectRootUri : vscode . Uri ,
92- ) : vscode . Uri {
93- let baseDirUri : vscode . Uri
83+ currentFileUri : string ,
84+ projectRootUri : string ,
85+ fs : FileSystem ,
86+ ) : string {
87+ let baseDirUri : string
9488 if ( importInfo . isRelative ) {
9589 // For relative imports, go up 'relativeDots' directories from current file
9690 baseDirUri = uriDirname ( currentFileUri )
@@ -102,7 +96,7 @@ function modulePathToDir(
10296 }
10397
10498 if ( importInfo . modulePath ) {
105- return vscode . Uri . joinPath ( baseDirUri , ...importInfo . modulePath . split ( "." ) )
99+ return fs . joinPath ( baseDirUri , ...importInfo . modulePath . split ( "." ) )
106100 }
107101 return baseDirUri
108102}
@@ -118,61 +112,68 @@ function modulePathToDir(
118112 */
119113export async function resolveImport (
120114 importInfo : Pick < ImportInfo , "modulePath" | "isRelative" | "relativeDots" > ,
121- currentFileUri : vscode . Uri ,
122- projectRootUri : vscode . Uri ,
123- ) : Promise < vscode . Uri | null > {
115+ currentFileUri : string ,
116+ projectRootUri : string ,
117+ fs : FileSystem ,
118+ ) : Promise < string | null > {
124119 const resolvedUri = modulePathToDir (
125120 importInfo ,
126121 currentFileUri ,
127122 projectRootUri ,
123+ fs ,
128124 )
129- return resolvePythonModule ( resolvedUri )
125+ return resolvePythonModule ( resolvedUri , fs )
130126}
131127
132128/**
133129 * Resolves a named import to its file URI.
134130 * For example, from .routes import users
135131 * will try to resolve to routes/users.py
132+ *
133+ * @param analyzeFileFn - Function to analyze a file (injected to avoid circular dependency)
136134 */
137135export async function resolveNamedImport (
138136 importInfo : Pick <
139137 ImportInfo ,
140138 "modulePath" | "names" | "isRelative" | "relativeDots"
141139 > ,
142- currentFileUri : vscode . Uri ,
143- projectRootUri : vscode . Uri ,
144- parser ?: Parser ,
145- ) : Promise < vscode . Uri | null > {
140+ currentFileUri : string ,
141+ projectRootUri : string ,
142+ fs : FileSystem ,
143+ analyzeFileFn ?: ( uri : string ) => Promise < { imports : ImportInfo [ ] } | null > ,
144+ ) : Promise < string | null > {
146145 const baseUri = await resolveImport (
147146 importInfo ,
148147 currentFileUri ,
149148 projectRootUri ,
149+ fs ,
150150 )
151151
152152 // Calculate base directory for named import resolution.
153153 // For namespace packages (directories without __init__.py), baseUri will be null,
154154 // so we compute the directory path directly from the module path.
155155 const baseDirUri = baseUri
156156 ? uriDirname ( baseUri )
157- : modulePathToDir ( importInfo , currentFileUri , projectRootUri )
157+ : modulePathToDir ( importInfo , currentFileUri , projectRootUri , fs )
158158
159159 for ( const name of importInfo . names ) {
160160 // Try direct file: from .routes import users -> routes/users.py
161- const namedUri = vscode . Uri . joinPath ( baseDirUri , ...name . split ( "." ) )
162- const resolved = await resolvePythonModule ( namedUri )
161+ const namedUri = fs . joinPath ( baseDirUri , ...name . split ( "." ) )
162+ const resolved = await resolvePythonModule ( namedUri , fs )
163163 if ( resolved ) {
164164 return resolved
165165 }
166166
167167 // Try re-exports: from .routes import users where routes/__init__.py re-exports users
168- if ( baseUri ?. path . endsWith ( "__init__.py" ) && parser ) {
169- const analysis = await analyzeFile ( baseUri , parser )
168+ if ( baseUri ?. endsWith ( "__init__.py" ) && analyzeFileFn ) {
169+ const analysis = await analyzeFileFn ( baseUri )
170170 const imp = analysis && findImportByExportedName ( analysis . imports , name )
171171 if ( imp ) {
172172 const reExportResolved = await resolveImport (
173173 imp ,
174174 baseUri ,
175175 projectRootUri ,
176+ fs ,
176177 )
177178 if ( reExportResolved ) {
178179 return reExportResolved
@@ -192,22 +193,28 @@ export async function resolveNamedImport(
192193 * For example, if integrations/__init__.py contains:
193194 * from .router import router as router
194195 * This will return the path to integrations/router.py
196+ *
197+ * @param analyzeFileFn - Function to analyze a file (injected to avoid circular dependency)
195198 */
196199export async function resolveRouterFromInit (
197- initFileUri : vscode . Uri ,
198- projectRootUri : vscode . Uri ,
199- parser : Parser ,
200- ) : Promise < vscode . Uri | null > {
201- if ( ! initFileUri . path . endsWith ( "__init__.py" ) ) {
200+ initFileUri : string ,
201+ projectRootUri : string ,
202+ fs : FileSystem ,
203+ analyzeFileFn : ( uri : string ) => Promise < {
204+ imports : ImportInfo [ ]
205+ routers : { variableName : string } [ ]
206+ } | null > ,
207+ ) : Promise < string | null > {
208+ if ( ! initFileUri . endsWith ( "__init__.py" ) ) {
202209 return null
203210 }
204211
205- const analysis = await analyzeFile ( initFileUri , parser )
212+ const analysis = await analyzeFileFn ( initFileUri )
206213 // If file has routers defined, no need to follow re-exports
207214 if ( ! analysis || analysis . routers . length > 0 ) {
208215 return null
209216 }
210217
211218 const imp = findImportByExportedName ( analysis . imports , "router" )
212- return imp ? resolveImport ( imp , initFileUri , projectRootUri ) : null
219+ return imp ? resolveImport ( imp , initFileUri , projectRootUri , fs ) : null
213220}
0 commit comments