11import fs from "node:fs" ;
22import path from 'node:path' ;
3- import os , { EOL } from "os" ;
3+ import os , { EOL } from "os" ;
44
5- import { environmentVariableIsPopulated , getCustom , invokeCommand } from "../tools.js" ;
5+ import { Language , Parser , Query } from 'web-tree-sitter' ;
6+
7+ import { environmentVariableIsPopulated , getCustom , invokeCommand } from "../tools.js" ;
68
79function getPipFreezeOutput ( ) {
810 try {
@@ -23,7 +25,6 @@ function getPipShowOutput(depNames) {
2325/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]} } DependencyEntry */
2426
2527export default class Python_controller {
26-
2728 pythonEnvDir
2829 pathToPipBin
2930 pathToPythonBin
@@ -39,14 +40,15 @@ export default class Python_controller {
3940 * @param {string } pathToRequirements
4041 * @
4142 */
42- constructor ( realEnvironment , pathToPip , pathToPython , pathToRequirements , options = { } ) {
43+ constructor ( realEnvironment , pathToPip , pathToPython , pathToRequirements , options = { } ) {
4344 this . pathToPythonBin = pathToPython
4445 this . pathToPipBin = pathToPip
4546 this . realEnvironment = realEnvironment
4647 this . prepareEnvironment ( )
4748 this . pathToRequirements = pathToRequirements
4849 this . options = options
4950 }
51+
5052 prepareEnvironment ( ) {
5153 if ( ! this . realEnvironment ) {
5254 this . pythonEnvDir = path . join ( path . sep , "tmp" , "trustify_da_env_js" )
@@ -87,6 +89,37 @@ export default class Python_controller {
8789 }
8890 }
8991
92+ /**
93+ * Parse the requirements.txt file using tree-sitter and return structured requirement data.
94+ * @return {Promise<{name: string, version: string|null}[]> }
95+ */
96+ async #parseRequirements( ) {
97+ const content = fs . readFileSync ( this . pathToRequirements ) . toString ( ) ;
98+
99+ await Parser . init ( {
100+ locateFile ( ) {
101+ return path . resolve ( import . meta. dirname , '../../node_modules/web-tree-sitter/web-tree-sitter.wasm' )
102+ }
103+ } ) ;
104+ const language = await Language . load ( path . resolve ( import . meta. dirname , '../../node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm' ) )
105+
106+ const parser = new Parser ( ) . setLanguage ( language ) ;
107+
108+ const requirementQuery = new Query ( language , '(requirement (package) @name) @req' ) ;
109+ const pinnedVersionQuery = new Query ( language , '(version_spec (version_cmp) @cmp (version) @version (#eq? @cmp "=="))' ) ;
110+
111+ const tree = parser . parse ( content ) ;
112+ return requirementQuery . matches ( tree . rootNode ) . map ( match => {
113+ const reqNode = match . captures . find ( c => c . name === 'req' ) . node ;
114+ const name = match . captures . find ( c => c . name === 'name' ) . node . text ;
115+ const versionMatches = pinnedVersionQuery . matches ( reqNode ) ;
116+ const version = versionMatches . length > 0
117+ ? versionMatches [ 0 ] . captures . find ( c => c . name === 'version' ) . node . text
118+ : null ;
119+ return { name, version } ;
120+ } ) ;
121+ }
122+
90123 #decideIfWindowsOrLinuxPath( fileName ) {
91124 if ( os . platform ( ) === "win32" ) {
92125 return fileName + ".exe"
@@ -97,9 +130,9 @@ export default class Python_controller {
97130 /**
98131 *
99132 * @param {boolean } includeTransitive - whether to return include in returned object transitive dependencies or not
100- * @return {[DependencyEntry] }
133+ * @return {Promise< [DependencyEntry]> }
101134 */
102- getDependencies ( includeTransitive ) {
135+ async getDependencies ( includeTransitive ) {
103136 let startingTime
104137 let endingTime
105138 if ( process . env [ "TRUSTIFY_DA_DEBUG" ] === "true" ) {
@@ -126,7 +159,7 @@ export default class Python_controller {
126159 this . #installingRequirementsOneByOne( )
127160 }
128161 }
129- let dependencies = this . #getDependenciesImpl( includeTransitive )
162+ let dependencies = await this . #getDependenciesImpl( includeTransitive )
130163 this . #cleanEnvironment( )
131164 if ( process . env [ "TRUSTIFY_DA_DEBUG" ] === "true" ) {
132165 endingTime = new Date ( )
@@ -137,15 +170,13 @@ export default class Python_controller {
137170 return dependencies
138171 }
139172
140- #installingRequirementsOneByOne( ) {
141- let requirementsContent = fs . readFileSync ( this . pathToRequirements ) ;
142- let requirementsRows = requirementsContent . toString ( ) . split ( EOL ) ;
143- requirementsRows . filter ( ( line ) => ! line . trim ( ) . startsWith ( "#" ) ) . filter ( ( line ) => line . trim ( ) !== "" ) . forEach ( ( dependency ) => {
144- let dependencyName = getDependencyName ( dependency ) ;
173+ async #installingRequirementsOneByOne( ) {
174+ const requirements = await this . #parseRequirements( ) ;
175+ requirements . forEach ( ( { name} ) => {
145176 try {
146- invokeCommand ( this . pathToPipBin , [ 'install' , dependencyName ] )
177+ invokeCommand ( this . pathToPipBin , [ 'install' , name ] )
147178 } catch ( error ) {
148- throw new Error ( `Failed in best-effort installing ${ dependencyName } in virtual python environment` , { cause : error } )
179+ throw new Error ( `Failed in best-effort installing ${ name } in virtual python environment` , { cause : error } )
149180 }
150181 } )
151182 }
@@ -162,44 +193,33 @@ export default class Python_controller {
162193 }
163194 }
164195
165- #getDependenciesImpl( includeTransitive ) {
166- let dependencies = new Array ( )
196+ async #getDependenciesImpl( includeTransitive ) {
197+ let dependencies = [ ]
167198 let usePipDepTree = getCustom ( "TRUSTIFY_DA_PIP_USE_DEP_TREE" , "false" , this . options ) ;
168- let freezeOutput
169- let lines
170- let depNames
171- let pipShowOutput
172199 let allPipShowDeps
173200 let pipDepTreeJsonArrayOutput
174201 if ( usePipDepTree !== "true" ) {
175- freezeOutput = getPipFreezeOutput . call ( this ) ;
176- lines = freezeOutput . split ( EOL )
177- depNames = lines . map ( line => getDependencyName ( line ) )
178- }
179- else {
180- pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree ( this . pathToPipBin , this . pathToPythonBin )
181- }
182-
183-
184- if ( usePipDepTree !== "true" ) {
185- pipShowOutput = getPipShowOutput . call ( this , depNames ) ;
202+ const freezeOutput = getPipFreezeOutput . call ( this ) ;
203+ const lines = freezeOutput . split ( EOL )
204+ const depNames = lines . map ( line => getDependencyName ( line ) )
205+ const pipShowOutput = getPipShowOutput . call ( this , depNames ) ;
186206 allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
207+ } else {
208+ pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree ( this . pathToPipBin , this . pathToPythonBin )
187209 }
188- //debug
189- // pipShowOutput = "alternative pip show output goes here for debugging"
190210
191211 let matchManifestVersions = getCustom ( "MATCH_MANIFEST_VERSIONS" , "true" , this . options ) ;
192- let linesOfRequirements = fs . readFileSync ( this . pathToRequirements ) . toString ( ) . split ( EOL ) . filter ( ( line ) => ! line . trim ( ) . startsWith ( "#" ) ) . map ( line => line . trim ( ) )
212+ let parsedRequirements = await this . #parseRequirements ( )
193213 let CachedEnvironmentDeps = { }
194214 if ( usePipDepTree !== "true" ) {
195- allPipShowDeps . forEach ( ( record ) => {
215+ allPipShowDeps . forEach ( record => {
196216 let dependencyName = getDependencyNameShow ( record ) . toLowerCase ( )
197217 CachedEnvironmentDeps [ dependencyName ] = record
198218 CachedEnvironmentDeps [ dependencyName . replace ( "-" , "_" ) ] = record
199219 CachedEnvironmentDeps [ dependencyName . replace ( "_" , "-" ) ] = record
200220 } )
201221 } else {
202- pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
222+ pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
203223 let packageName = depTreeEntry [ "package" ] [ "package_name" ] . toLowerCase ( )
204224 let pipDepTreeEntryForCache = {
205225 name : packageName ,
@@ -211,41 +231,25 @@ export default class Python_controller {
211231 CachedEnvironmentDeps [ packageName . replace ( "_" , "-" ) ] = pipDepTreeEntryForCache
212232 } )
213233 }
214- linesOfRequirements . forEach ( ( dep ) => {
215- // if matchManifestVersions setting is turned on , then
216- if ( matchManifestVersions === "true" ) {
217- let dependencyName
218- let manifestVersion
234+ parsedRequirements . forEach ( ( { name : depName , version : manifestVersion } ) => {
235+ if ( matchManifestVersions === "true" && manifestVersion != null ) {
219236 let installedVersion
220- let doubleEqualSignPosition
221- if ( dep . includes ( "==" ) ) {
222- doubleEqualSignPosition = dep . indexOf ( "==" )
223- manifestVersion = dep . substring ( doubleEqualSignPosition + 2 ) . trim ( )
224- if ( manifestVersion . includes ( "#" ) ) {
225- let hashCharIndex = manifestVersion . indexOf ( "#" ) ;
226- manifestVersion = manifestVersion . substring ( 0 , hashCharIndex )
237+ if ( CachedEnvironmentDeps [ depName . toLowerCase ( ) ] !== undefined ) {
238+ if ( usePipDepTree !== "true" ) {
239+ installedVersion = getDependencyVersion ( CachedEnvironmentDeps [ depName . toLowerCase ( ) ] )
240+ } else {
241+ installedVersion = CachedEnvironmentDeps [ depName . toLowerCase ( ) ] . version
227242 }
228- dependencyName = getDependencyName ( dep )
229- // only compare between declared version in manifest to installed version , if the package is installed.
230- if ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] !== undefined ) {
231- if ( usePipDepTree !== "true" ) {
232- installedVersion = getDependencyVersion ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] )
233- } else {
234- installedVersion = CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] . version
235- }
236- }
237- if ( installedVersion ) {
238- if ( manifestVersion . trim ( ) !== installedVersion . trim ( ) ) {
239- throw new Error ( `Can't continue with analysis - versions mismatch for dependency name ${ dependencyName } (manifest version=${ manifestVersion } , installed version=${ installedVersion } ).If you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting MATCH_MANIFEST_VERSIONS=false` )
240- }
243+ }
244+ if ( installedVersion ) {
245+ if ( manifestVersion . trim ( ) !== installedVersion . trim ( ) ) {
246+ throw new Error ( `Can't continue with analysis - versions mismatch for dependency name ${ depName } (manifest version=${ manifestVersion } , installed version=${ installedVersion } ).If you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting MATCH_MANIFEST_VERSIONS=false` )
241247 }
242248 }
243249 }
244- let path = new Array ( )
245- let depName = getDependencyName ( dep )
246- //array to track a path for each branch in the dependency tree
250+ let path = [ ]
247251 path . push ( depName . toLowerCase ( ) )
248- bringAllDependencies ( dependencies , depName , CachedEnvironmentDeps , includeTransitive , path , usePipDepTree )
252+ bringAllDependencies ( dependencies , depName , CachedEnvironmentDeps , includeTransitive , path , usePipDepTree )
249253 } )
250254 dependencies . sort ( ( dep1 , dep2 ) => {
251255 const DEP1 = dep1 . name . toLowerCase ( )
@@ -350,12 +354,12 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350354 version = record . version
351355 directDeps = record . dependencies
352356 }
353- let targetDeps = new Array ( )
357+ let targetDeps = [ ]
354358
355- let entry = { "name" : depName , "version" : version , "dependencies" : [ ] }
359+ let entry = { "name" : depName , "version" : version , "dependencies" : [ ] }
356360 dependencies . push ( entry )
357361 directDeps . forEach ( ( dep ) => {
358- let depArray = new Array ( )
362+ let depArray = [ ]
359363 // to avoid infinite loop, check if the dependency not already on current path, before going recursively resolving its dependencies.
360364 if ( ! path . includes ( dep . toLowerCase ( ) ) ) {
361365 // send to recurrsion the path + the current dep
0 commit comments