Skip to content

Commit cd46b6f

Browse files
committed
wip
1 parent aba4457 commit cd46b6f

8 files changed

Lines changed: 433 additions & 251 deletions

File tree

package-lock.json

Lines changed: 300 additions & 114 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"https-proxy-agent": "^7.0.6",
5454
"node-fetch": "^3.3.2",
5555
"packageurl-js": "~1.0.2",
56+
"tree-sitter-requirements": "github:Strum355/tree-sitter-requirements",
57+
"web-tree-sitter": "^0.26.6",
5658
"yargs": "^18.0.0"
5759
},
5860
"devDependencies": {
@@ -64,6 +66,7 @@
6466
"c8": "^11.0.0",
6567
"chai": "^4.3.7",
6668
"eslint": "^8.42.0",
69+
"eslint-import-resolver-typescript": "^4.4.4",
6770
"eslint-plugin-editorconfig": "^4.0.3",
6871
"eslint-plugin-import": "^2.29.1",
6972
"esmock": "^2.6.2",

src/analysis.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function addProxyAgent(options, opts) {
4141
async function requestStack(provider, manifest, url, html = false, opts = {}) {
4242
opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64')
4343
opts["manifest-type"] = path.parse(manifest).base
44-
let provided = provider.provideStack(manifest, opts) // throws error if content providing failed
44+
let provided = await provider.provideStack(manifest, opts) // throws error if content providing failed
4545
opts["source-manifest"] = ""
4646
opts[rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_")] = "stack-analysis"
4747
let startTime = new Date()
@@ -105,7 +105,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
105105
async function requestComponent(provider, manifest, url, opts = {}) {
106106
opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64')
107107

108-
let provided = provider.provideComponent(manifest, opts) // throws error if content providing failed
108+
let provided = await provider.provideComponent(manifest, opts) // throws error if content providing failed
109109
opts["source-manifest"] = ""
110110
opts[rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_")] = "component-analysis"
111111
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {

src/provider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Javascript_yarn from './providers/javascript_yarn.js';
1010
import pythonPipProvider from './providers/python_pip.js'
1111

1212
/** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
13-
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided, provideStack: function(string, {}): Provided}} Provider */
13+
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>}} Provider */
1414

1515
/**
1616
* MUST include all providers here.

src/providers/python_controller.js

Lines changed: 72 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import fs from "node:fs";
22
import 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

79
function getPipFreezeOutput() {
810
try {
@@ -23,7 +25,6 @@ function getPipShowOutput(depNames) {
2325
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
2426

2527
export 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

Comments
 (0)