1
1
import Parser from "tree-sitter" ;
2
- import { PythonExportResolver } from "../exportResolver" ;
3
- import { ImportStatement , PythonImportResolver } from "../importResolver" ;
2
+ import { PythonExportExtractor } from "../exportExtractor" ;
4
3
import { PythonUsageResolver } from "../usageResolver" ;
5
4
6
5
/**
@@ -13,6 +12,14 @@ export interface ModuleDependency {
13
12
symbols : Map < string , string > ;
14
13
}
15
14
15
+ export interface SymbolDependency {
16
+ id : string ;
17
+ characterCount : number ;
18
+ lineCount : number ;
19
+ type : string ;
20
+ dependencies : Map < string , ModuleDependency > ;
21
+ }
22
+
16
23
/**
17
24
* Represents the dependency information for a file.
18
25
* Contains file-level dependencies and per-exported-symbol dependencies.
@@ -27,179 +34,40 @@ export interface FileDependencies {
27
34
// Map of module dependencies detected at the file level.
28
35
dependencies : Map < string , ModuleDependency > ;
29
36
// Array of dependencies specific to each exported symbol.
30
- symbols : {
31
- id : string ;
32
- characterCount : number ;
33
- lineCount : number ;
34
- type : string ;
35
- // Dependencies detected within the exported symbol's AST subtree.
36
- dependencies : Map < string , ModuleDependency > ;
37
- } [ ] ;
37
+ symbols : SymbolDependency [ ] ;
38
38
}
39
39
40
40
/**
41
41
* PythonDependencyResolver analyzes a Python file's AST to build a dependency manifesto.
42
- * It uses the ExportResolver to determine exported symbols, the ImportResolver to extract import statements,
43
- * and the UsageResolver to determine which parts of an import are actually used.
42
+ * It uses the PythonExportExtractor to determine exported symbols and the PythonUsageResolver
43
+ * to determine which imports are used and how they are used within the file .
44
44
*
45
45
* Dependencies are computed at two levels:
46
46
*
47
47
* 1. File-level: Based on the file's root AST node.
48
48
* 2. Symbol-level: For each exported symbol, by analyzing its AST subtree.
49
49
*
50
+ * The resolver distinguishes between internal (project) and external dependencies,
51
+ * and tracks which symbols from each module are actually used.
52
+ *
50
53
* Results are cached to avoid re-computation.
51
54
*/
52
55
export class PythonDependencyResolver {
53
- private parser : Parser ;
54
56
private files : Map < string , { path : string ; rootNode : Parser . SyntaxNode } > ;
55
- private exportResolver : PythonExportResolver ;
56
- private importResolver : PythonImportResolver ;
57
+ private exportExtractor : PythonExportExtractor ;
57
58
private usageResolver : PythonUsageResolver ;
58
59
private cache = new Map < string , FileDependencies > ( ) ;
59
60
60
61
constructor (
61
- parser : Parser ,
62
62
files : Map < string , { path : string ; rootNode : Parser . SyntaxNode } > ,
63
- exportResolver : PythonExportResolver ,
64
- importResolver : PythonImportResolver ,
63
+ exportExtractor : PythonExportExtractor ,
65
64
usageResolver : PythonUsageResolver ,
66
65
) {
67
- this . parser = parser ;
68
66
this . files = files ;
69
- this . exportResolver = exportResolver ;
70
- this . importResolver = importResolver ;
67
+ this . exportExtractor = exportExtractor ;
71
68
this . usageResolver = usageResolver ;
72
69
}
73
70
74
- /**
75
- * Returns AST nodes to exclude when checking for usage (e.g. the nodes corresponding to import statements).
76
- *
77
- * @param targetNode The AST node to analyze.
78
- * @returns An array of nodes to exclude from usage queries.
79
- */
80
- private getNodeToExcludeFromUsage (
81
- targetNode : Parser . SyntaxNode ,
82
- ) : Parser . SyntaxNode [ ] {
83
- const query = new Parser . Query (
84
- this . parser . getLanguage ( ) ,
85
- `
86
- ([
87
- (import_from_statement) @imp
88
- (import_statement) @imp
89
- ])
90
- ` ,
91
- ) ;
92
- const captures = query . captures ( targetNode ) ;
93
- return captures . map ( ( { node } ) => node ) ;
94
- }
95
-
96
- /**
97
- * Analyzes a target AST node (either the file root or an exported symbol's node) to determine module dependencies.
98
- *
99
- * For each resolved import statement module, this function:
100
- * - Gets nodes to exclude from usage queries (e.g. import statement nodes).
101
- * - For internal modules (with a filePath), builds a module info object (including explicit symbols)
102
- * and calls resolveUsageForInternalModule.
103
- * - For external modules, calls resolveUsageForExternalModule.
104
- *
105
- * The usage resolver returns a map where each key is the module's filePath or fullName and
106
- * each value contains an array of used symbols. This function then merges these results into a
107
- * map of ModuleDependency objects.
108
- *
109
- * @param targetNode The AST node to analyze.
110
- * @param importStatements An array of resolved import statements for the file.
111
- * @returns A map of module dependency information keyed by module id.
112
- */
113
- private getTargetNodeDependencies (
114
- targetNode : Parser . SyntaxNode ,
115
- importStatements : ImportStatement [ ] ,
116
- ) : Map < string , ModuleDependency > {
117
- const fileDependencies = new Map < string , ModuleDependency > ( ) ;
118
-
119
- // Process each import statement.
120
- importStatements . forEach ( ( importStmt ) => {
121
- // Process each resolved module from the statement.
122
- importStmt . modules . forEach ( ( importModule ) => {
123
- // Exclude the nodes (e.g. import statements) from the usage query.
124
- const nodesToExclude = this . getNodeToExcludeFromUsage ( targetNode ) ;
125
-
126
- if ( importModule . module ) {
127
- // Internal module: build a module info object including explicit symbols.
128
- const moduleInfo = {
129
- identifier : importModule . source ,
130
- alias : importModule . alias ,
131
- moduleNode : importModule . module ,
132
- explicitSymbols : importModule . symbols . map ( ( s ) => ( {
133
- identifier : s . id ,
134
- alias : s . alias ,
135
- } ) ) ,
136
- } ;
137
-
138
- // Resolve usage for the internal module within the target AST.
139
- const usage = this . usageResolver . resolveUsageForInternalModule (
140
- targetNode ,
141
- moduleInfo ,
142
- nodesToExclude ,
143
- ) ;
144
-
145
- if ( usage . size > 0 ) {
146
- // For each usage result, merge the used symbols into the dependency map.
147
- usage . forEach ( ( usageResult ) => {
148
- const key =
149
- usageResult . moduleNode . filePath ||
150
- usageResult . moduleNode . fullName ;
151
- let fileDep = fileDependencies . get ( key ) ;
152
- if ( ! fileDep ) {
153
- fileDep = {
154
- id : key ,
155
- isExternal : false ,
156
- symbols : new Map ( ) ,
157
- } ;
158
- }
159
- // For each symbol used, add it to the dependency's symbol map.
160
- usageResult . symbols . forEach ( ( sym ) => {
161
- fileDep . symbols . set ( sym , sym ) ;
162
- } ) ;
163
- fileDependencies . set ( key , fileDep ) ;
164
- } ) ;
165
- }
166
- } else {
167
- // External module: build a module info object (without a moduleNode).
168
- const moduleInfo = {
169
- identifier : importModule . source ,
170
- alias : importModule . alias ,
171
- explicitSymbols : importModule . symbols . map ( ( s ) => ( {
172
- identifier : s . id ,
173
- alias : s . alias ,
174
- } ) ) ,
175
- } ;
176
- // Resolve usage for the external module.
177
- const extUsage = this . usageResolver . resolveUsageForExternalModule (
178
- targetNode ,
179
- moduleInfo ,
180
- nodesToExclude ,
181
- ) ;
182
- if ( extUsage ) {
183
- let fileDep = fileDependencies . get ( extUsage . moduleName ) ;
184
- if ( ! fileDep ) {
185
- fileDep = {
186
- id : extUsage . moduleName ,
187
- isExternal : true ,
188
- symbols : new Map ( ) ,
189
- } ;
190
- }
191
- extUsage . symbols . forEach ( ( sym ) => {
192
- fileDep . symbols . set ( sym , sym ) ;
193
- } ) ;
194
- fileDependencies . set ( fileDep . id , fileDep ) ;
195
- }
196
- }
197
- } ) ;
198
- } ) ;
199
-
200
- return fileDependencies ;
201
- }
202
-
203
71
/**
204
72
* Constructs the complete dependency manifesto for a given file.
205
73
*
@@ -226,18 +94,6 @@ export class PythonDependencyResolver {
226
94
throw new Error ( `File not found: ${ filePath } ` ) ;
227
95
}
228
96
229
- // Retrieve exported symbols from the file.
230
- const exportedSymbols = this . exportResolver . getSymbols ( filePath ) ;
231
- // Retrieve all import statements from the file.
232
- const importedStatements =
233
- this . importResolver . getImportStatements ( filePath ) ;
234
-
235
- // Compute file-level dependencies using the file's root AST node.
236
- const dependencies = this . getTargetNodeDependencies (
237
- file . rootNode ,
238
- importedStatements ,
239
- ) ;
240
-
241
97
const characterCount = file . rootNode . endIndex - file . rootNode . startIndex ;
242
98
const lineCount =
243
99
file . rootNode . endPosition . row - file . rootNode . startPosition . row + 1 ;
@@ -246,63 +102,89 @@ export class PythonDependencyResolver {
246
102
filePath,
247
103
characterCount,
248
104
lineCount,
249
- dependencies,
105
+ dependencies : new Map < string , ModuleDependency > ( ) ,
250
106
symbols : [ ] ,
251
107
} ;
252
108
253
- // For each exported symbol, compute its dependencies using the symbol's AST subtree.
254
- exportedSymbols . forEach ( ( symbol ) => {
255
- // Get the symbol's dependencies from the import statements.
256
- const symDependencies = this . getTargetNodeDependencies (
257
- symbol . node ,
258
- importedStatements ,
259
- ) ;
260
-
261
- // Check if other symbols are used in the symbol's
262
- exportedSymbols . forEach ( ( otherSymbol ) => {
263
- if ( otherSymbol . id === symbol . id ) {
264
- return ;
265
- }
109
+ const fileUsage = this . usageResolver . resolveUsage ( file . path , file . rootNode ) ;
110
+
111
+ fileUsage . internal . forEach ( ( internal ) => {
112
+ const dependency : ModuleDependency = {
113
+ id : internal . moduleNode . path ,
114
+ isExternal : false ,
115
+ symbols : new Map ( ) ,
116
+ } ;
117
+ if ( internal . symbols ) {
118
+ internal . symbols . forEach ( ( symbol ) => {
119
+ dependency . symbols . set ( symbol . id , symbol . id ) ;
120
+ } ) ;
121
+ }
122
+ fileDependencyManifesto . dependencies . set ( dependency . id , dependency ) ;
123
+ } ) ;
266
124
267
- const nodesToExclude = this . getNodeToExcludeFromUsage ( symbol . node ) ;
125
+ fileUsage . external . forEach ( ( external ) => {
126
+ const dependency : ModuleDependency = {
127
+ id : external . moduleName ,
128
+ isExternal : true ,
129
+ symbols : new Map ( ) ,
130
+ } ;
131
+ if ( external . symbolNames ) {
132
+ external . symbolNames . forEach ( ( symbol ) => {
133
+ dependency . symbols . set ( symbol , symbol ) ;
134
+ } ) ;
135
+ }
136
+ fileDependencyManifesto . dependencies . set ( dependency . id , dependency ) ;
137
+ } ) ;
268
138
269
- const isUsed = this . usageResolver . isRefUsed (
270
- symbol . node ,
271
- nodesToExclude ,
272
- otherSymbol . identifierNode . text ,
273
- ) ;
139
+ const fileSymbols = this . exportExtractor . getSymbols ( filePath ) ;
140
+
141
+ fileSymbols . symbols . forEach ( ( fileSymbol ) => {
142
+ const characterCount =
143
+ fileSymbol . node . endIndex - fileSymbol . node . startIndex ;
144
+ const lineCount =
145
+ fileSymbol . node . endPosition . row - fileSymbol . node . startPosition . row + 1 ;
146
+
147
+ const SymbolDependency : SymbolDependency = {
148
+ id : fileSymbol . id ,
149
+ characterCount,
150
+ lineCount,
151
+ type : fileSymbol . type ,
152
+ dependencies : new Map < string , ModuleDependency > ( ) ,
153
+ } ;
154
+
155
+ const symbolUsage = this . usageResolver . resolveUsage (
156
+ file . path ,
157
+ fileSymbol . node ,
158
+ ) ;
274
159
275
- if ( isUsed ) {
276
- // get/add current file to dependency
277
- let fileDep = symDependencies . get ( filePath ) ;
278
- if ( ! fileDep ) {
279
- fileDep = {
280
- id : filePath ,
281
- isExternal : false ,
282
- symbols : new Map ( ) ,
283
- } ;
284
- }
285
- // get/add other symbol to dependency
286
- let otherSymbolDep = fileDep . symbols . get ( otherSymbol . id ) ;
287
- if ( ! otherSymbolDep ) {
288
- otherSymbolDep = otherSymbol . id ;
289
- }
290
- fileDep . symbols . set ( otherSymbol . id , otherSymbolDep ) ;
291
- symDependencies . set ( fileDep . id , fileDep ) ;
160
+ symbolUsage . internal . forEach ( ( internal ) => {
161
+ const dependency : ModuleDependency = {
162
+ id : internal . moduleNode . path ,
163
+ isExternal : false ,
164
+ symbols : new Map ( ) ,
165
+ } ;
166
+ if ( internal . symbols ) {
167
+ internal . symbols . forEach ( ( symbol ) => {
168
+ dependency . symbols . set ( symbol . id , symbol . id ) ;
169
+ } ) ;
292
170
}
171
+ SymbolDependency . dependencies . set ( dependency . id , dependency ) ;
293
172
} ) ;
294
173
295
- const symbolCharacterCount =
296
- symbol . node . endIndex - symbol . node . startIndex ;
297
- const symbolLineCount =
298
- symbol . node . endPosition . row - symbol . node . startPosition . row + 1 ;
299
- fileDependencyManifesto . symbols . push ( {
300
- id : symbol . id ,
301
- characterCount : symbolCharacterCount ,
302
- lineCount : symbolLineCount ,
303
- type : symbol . type ,
304
- dependencies : symDependencies ,
174
+ symbolUsage . external . forEach ( ( external ) => {
175
+ const dependency : ModuleDependency = {
176
+ id : external . moduleName ,
177
+ isExternal : true ,
178
+ symbols : new Map ( ) ,
179
+ } ;
180
+ if ( external . symbolNames ) {
181
+ external . symbolNames . forEach ( ( symbol ) => {
182
+ dependency . symbols . set ( symbol , symbol ) ;
183
+ } ) ;
184
+ }
185
+ SymbolDependency . dependencies . set ( dependency . id , dependency ) ;
305
186
} ) ;
187
+ fileDependencyManifesto . symbols . push ( SymbolDependency ) ;
306
188
} ) ;
307
189
308
190
this . cache . set ( cacheKey , fileDependencyManifesto ) ;
0 commit comments