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 { environmentVariableIsPopulated , getCustom , invokeCommand } from "../tools.js" ;
6+
7+ import { getParser , getRequirementQuery , getPinnedVersionQuery } from './requirements_parser.js' ;
68
79function getPipFreezeOutput ( ) {
810 try {
@@ -23,13 +25,15 @@ 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
3031 realEnvironment
3132 pathToRequirements
3233 options
34+ parser
35+ requirementsQuery
36+ pinnedVersionQuery
3337
3438 /**
3539 * Constructor to create new python controller instance to interact with pip package manager
@@ -39,14 +43,18 @@ export default class Python_controller {
3943 * @param {string } pathToRequirements
4044 * @
4145 */
42- constructor ( realEnvironment , pathToPip , pathToPython , pathToRequirements , options = { } ) {
46+ constructor ( realEnvironment , pathToPip , pathToPython , pathToRequirements , options = { } ) {
4347 this . pathToPythonBin = pathToPython
4448 this . pathToPipBin = pathToPip
4549 this . realEnvironment = realEnvironment
4650 this . prepareEnvironment ( )
4751 this . pathToRequirements = pathToRequirements
4852 this . options = options
53+ this . parser = getParser ( )
54+ this . requirementsQuery = getRequirementQuery ( )
55+ this . pinnedVersionQuery = getPinnedVersionQuery ( )
4956 }
57+
5058 prepareEnvironment ( ) {
5159 if ( ! this . realEnvironment ) {
5260 this . pythonEnvDir = path . join ( path . sep , "tmp" , "trustify_da_env_js" )
@@ -87,6 +95,25 @@ export default class Python_controller {
8795 }
8896 }
8997
98+ /**
99+ * Parse the requirements.txt file using tree-sitter and return structured requirement data.
100+ * @return {Promise<{name: string, version: string|null}[]> }
101+ */
102+ async #parseRequirements( ) {
103+ const content = fs . readFileSync ( this . pathToRequirements ) . toString ( ) ;
104+ const tree = ( await this . parser ) . parse ( content ) ;
105+ return Promise . all ( ( await this . requirementsQuery ) . matches ( tree . rootNode ) . map ( async ( match ) => {
106+ const reqNode = match . captures . find ( c => c . name === 'req' ) . node ;
107+ const name = match . captures . find ( c => c . name === 'name' ) . node . text ;
108+ const versionMatches = ( await this . pinnedVersionQuery ) . matches ( reqNode ) ;
109+ const version = versionMatches . length > 0
110+ ? versionMatches [ 0 ] . captures . find ( c => c . name === 'version' ) . node . text
111+ : null ;
112+ console . log ( `BANANA ${ name } ${ version } ` )
113+ return { name, version } ;
114+ } ) ) ;
115+ }
116+
90117 #decideIfWindowsOrLinuxPath( fileName ) {
91118 if ( os . platform ( ) === "win32" ) {
92119 return fileName + ".exe"
@@ -97,9 +124,9 @@ export default class Python_controller {
97124 /**
98125 *
99126 * @param {boolean } includeTransitive - whether to return include in returned object transitive dependencies or not
100- * @return {[DependencyEntry] }
127+ * @return {Promise< [DependencyEntry]> }
101128 */
102- getDependencies ( includeTransitive ) {
129+ async getDependencies ( includeTransitive ) {
103130 let startingTime
104131 let endingTime
105132 if ( process . env [ "TRUSTIFY_DA_DEBUG" ] === "true" ) {
@@ -126,7 +153,7 @@ export default class Python_controller {
126153 this . #installingRequirementsOneByOne( )
127154 }
128155 }
129- let dependencies = this . #getDependenciesImpl( includeTransitive )
156+ let dependencies = await this . #getDependenciesImpl( includeTransitive )
130157 this . #cleanEnvironment( )
131158 if ( process . env [ "TRUSTIFY_DA_DEBUG" ] === "true" ) {
132159 endingTime = new Date ( )
@@ -137,15 +164,13 @@ export default class Python_controller {
137164 return dependencies
138165 }
139166
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 ) ;
167+ async #installingRequirementsOneByOne( ) {
168+ const requirements = await this . #parseRequirements( ) ;
169+ requirements . forEach ( ( { name} ) => {
145170 try {
146- invokeCommand ( this . pathToPipBin , [ 'install' , dependencyName ] )
171+ invokeCommand ( this . pathToPipBin , [ 'install' , name ] )
147172 } catch ( error ) {
148- throw new Error ( `Failed in best-effort installing ${ dependencyName } in virtual python environment` , { cause : error } )
173+ throw new Error ( `Failed in best-effort installing ${ name } in virtual python environment` , { cause : error } )
149174 }
150175 } )
151176 }
@@ -162,44 +187,33 @@ export default class Python_controller {
162187 }
163188 }
164189
165- #getDependenciesImpl( includeTransitive ) {
166- let dependencies = new Array ( )
190+ async #getDependenciesImpl( includeTransitive ) {
191+ let dependencies = [ ]
167192 let usePipDepTree = getCustom ( "TRUSTIFY_DA_PIP_USE_DEP_TREE" , "false" , this . options ) ;
168- let freezeOutput
169- let lines
170- let depNames
171- let pipShowOutput
172193 let allPipShowDeps
173194 let pipDepTreeJsonArrayOutput
174195 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 ) ;
196+ const freezeOutput = getPipFreezeOutput . call ( this ) ;
197+ const lines = freezeOutput . split ( EOL )
198+ const depNames = lines . map ( line => getDependencyName ( line ) )
199+ const pipShowOutput = getPipShowOutput . call ( this , depNames ) ;
186200 allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
201+ } else {
202+ pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree ( this . pathToPipBin , this . pathToPythonBin )
187203 }
188- //debug
189- // pipShowOutput = "alternative pip show output goes here for debugging"
190204
191205 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 ( ) )
206+ let parsedRequirements = await this . #parseRequirements ( )
193207 let CachedEnvironmentDeps = { }
194208 if ( usePipDepTree !== "true" ) {
195- allPipShowDeps . forEach ( ( record ) => {
209+ allPipShowDeps . forEach ( record => {
196210 let dependencyName = getDependencyNameShow ( record ) . toLowerCase ( )
197211 CachedEnvironmentDeps [ dependencyName ] = record
198212 CachedEnvironmentDeps [ dependencyName . replace ( "-" , "_" ) ] = record
199213 CachedEnvironmentDeps [ dependencyName . replace ( "_" , "-" ) ] = record
200214 } )
201215 } else {
202- pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
216+ pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
203217 let packageName = depTreeEntry [ "package" ] [ "package_name" ] . toLowerCase ( )
204218 let pipDepTreeEntryForCache = {
205219 name : packageName ,
@@ -211,41 +225,25 @@ export default class Python_controller {
211225 CachedEnvironmentDeps [ packageName . replace ( "_" , "-" ) ] = pipDepTreeEntryForCache
212226 } )
213227 }
214- linesOfRequirements . forEach ( ( dep ) => {
215- // if matchManifestVersions setting is turned on , then
216- if ( matchManifestVersions === "true" ) {
217- let dependencyName
218- let manifestVersion
228+ parsedRequirements . forEach ( ( { name : depName , version : manifestVersion } ) => {
229+ if ( matchManifestVersions === "true" && manifestVersion != null ) {
219230 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 )
231+ if ( CachedEnvironmentDeps [ depName . toLowerCase ( ) ] !== undefined ) {
232+ if ( usePipDepTree !== "true" ) {
233+ installedVersion = getDependencyVersion ( CachedEnvironmentDeps [ depName . toLowerCase ( ) ] )
234+ } else {
235+ installedVersion = CachedEnvironmentDeps [ depName . toLowerCase ( ) ] . version
227236 }
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- }
237+ }
238+ if ( installedVersion ) {
239+ if ( manifestVersion . trim ( ) !== installedVersion . trim ( ) ) {
240+ 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` )
241241 }
242242 }
243243 }
244- let path = new Array ( )
245- let depName = getDependencyName ( dep )
246- //array to track a path for each branch in the dependency tree
244+ let path = [ ]
247245 path . push ( depName . toLowerCase ( ) )
248- bringAllDependencies ( dependencies , depName , CachedEnvironmentDeps , includeTransitive , path , usePipDepTree )
246+ bringAllDependencies ( dependencies , depName , CachedEnvironmentDeps , includeTransitive , path , usePipDepTree )
249247 } )
250248 dependencies . sort ( ( dep1 , dep2 ) => {
251249 const DEP1 = dep1 . name . toLowerCase ( )
@@ -350,12 +348,12 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350348 version = record . version
351349 directDeps = record . dependencies
352350 }
353- let targetDeps = new Array ( )
351+ let targetDeps = [ ]
354352
355- let entry = { "name" : depName , "version" : version , "dependencies" : [ ] }
353+ let entry = { "name" : depName , "version" : version , "dependencies" : [ ] }
356354 dependencies . push ( entry )
357355 directDeps . forEach ( ( dep ) => {
358- let depArray = new Array ( )
356+ let depArray = [ ]
359357 // to avoid infinite loop, check if the dependency not already on current path, before going recursively resolving its dependencies.
360358 if ( ! path . includes ( dep . toLowerCase ( ) ) ) {
361359 // send to recurrsion the path + the current dep
0 commit comments