From 0207aae88a50a3669957a74172fb2c7526945ab0 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Thu, 2 May 2024 11:38:21 +0300 Subject: [PATCH] feat: gradle support (#256) * feat: added Gradle support Signed-off-by: Ilona Shishov * chore: added args to full stack analysis code action Signed-off-by: Ilona Shishov * docs: add and update typeDoc annotations Signed-off-by: Ilona Shishov * test: update unit tests Signed-off-by: Ilona Shishov --------- Signed-off-by: Ilona Shishov --- package-lock.json | 14 +- package.json | 2 +- src/codeActionHandler.ts | 1 + src/constants.ts | 18 ++ src/dependencyAnalysis/collector.ts | 24 +- src/dependencyAnalysis/diagnostics.ts | 12 +- src/diagnosticsPipeline.ts | 5 +- src/fileHandler.ts | 5 + src/providers/build.gradle.ts | 268 +++++++++++++++++ src/providers/go.mod.ts | 3 +- src/providers/package.json.ts | 3 +- src/providers/pom.xml.ts | 4 +- src/providers/requirements.txt.ts | 3 +- test/codeActionHandler.test.ts | 3 +- test/dependencyAnalysis/collector.test.ts | 57 +++- test/providers/build.gradle.test.ts | 347 ++++++++++++++++++++++ 16 files changed, 743 insertions(+), 26 deletions(-) create mode 100644 src/providers/build.gradle.ts create mode 100644 test/providers/build.gradle.test.ts diff --git a/package-lock.json b/package-lock.json index 546b23cb..4ed73bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.9.4-ea.14", "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.29", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.34", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -837,13 +837,14 @@ } }, "node_modules/@RHEcosystemAppEng/exhort-javascript-api": { - "version": "0.1.1-ea.29", - "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.1-ea.29/31233280c6b9c2f59a81326e659e9f2b87c9b1c4", - "integrity": "sha512-Xx/4wzwXuQWr2+N1NNboIeyUv5qJr9MQWew3E6ONyTYWYuP223JTyndiWBNrPey/b6t6n1ojqu6hwNCo3TjVMg==", + "version": "0.1.1-ea.34", + "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.1-ea.34/51dc7d98877e2f707a4fe1aae177e5ac54932882", + "integrity": "sha512-kux7STDPWuGQHZN9/I7Jm5IBlS+lrWatCjQR8Udh2lVwOU5uJBcgB8oKvzX1XfkTznQPHikRq3fPaDpy4neLjg==", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.23.2", "@cyclonedx/cyclonedx-library": "^4.0.0", + "fast-toml": "^0.5.4", "fast-xml-parser": "^4.2.4", "help": "^3.0.2", "packageurl-js": "^1.0.2", @@ -2340,6 +2341,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-toml": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/fast-toml/-/fast-toml-0.5.4.tgz", + "integrity": "sha512-nUX94P84wE3gcem12g4FAH3xV7BmcCoV8FXcFVV+NIQAOwRJnyqVKhdGd9DqI4CunW+wOZhVBGKI6Jut/OlLTw==" + }, "node_modules/fast-xml-parser": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", diff --git a/package.json b/package.json index ca34a446..efca31b7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dist" ], "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.29", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.34", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", diff --git a/src/codeActionHandler.ts b/src/codeActionHandler.ts index 8ef64ec9..ebd80b20 100644 --- a/src/codeActionHandler.ts +++ b/src/codeActionHandler.ts @@ -79,6 +79,7 @@ function generateFullStackAnalysisAction(): CodeAction { command: { title: 'Analytics Report', command: globalConfig.stackAnalysisCommand, + arguments: ['', true], } }; } diff --git a/src/constants.ts b/src/constants.ts index f621e039..8a490a85 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,3 +16,21 @@ export const RHDA_DIAGNOSTIC_SOURCE = 'Red Hat Dependency Analytics Plugin'; * Placeholder used as a version for dependency templates. */ export const VERSION_PLACEHOLDER: string = '__VERSION__'; +/** + * Represents provider ecosystem names. + */ +export const GRADLE = 'gradle'; +export const MAVEN = 'maven'; +export const GOLANG = 'golang'; +export const NPM = 'npm'; +export const PYPI = 'pypi'; +/** + * An object mapping ecosystem names to their true ecosystems. + */ +export const ecosystemNameMappings: { [key: string]: string } = { + [GRADLE]: MAVEN, + [MAVEN]: MAVEN, + [GOLANG]: GOLANG, + [NPM]: NPM, + [PYPI]: PYPI, +}; \ No newline at end of file diff --git a/src/dependencyAnalysis/collector.ts b/src/dependencyAnalysis/collector.ts index 8c69cdc8..baf288c5 100644 --- a/src/dependencyAnalysis/collector.ts +++ b/src/dependencyAnalysis/collector.ts @@ -8,6 +8,7 @@ import { Range } from 'vscode-languageserver'; import { IPositionedString, IPositionedContext, IPosition } from '../positionTypes'; import { isDefined } from '../utils'; +import { ecosystemNameMappings, GRADLE } from '../constants'; /** * Represents a dependency specification. @@ -35,8 +36,11 @@ export class Dependency implements IDependency { */ export class DependencyMap { mapper: Map; - constructor(deps: IDependency[]) { - this.mapper = new Map(deps.map(d => [d.name.value, d])); + constructor(deps: IDependency[], ecosystem: string) { + this.mapper = new Map(deps.map(d => { + const key = ecosystem === GRADLE && d.version ? `${d.name.value}@${d.version.value}` : d.name.value; + return [key, d]; + })); } /** @@ -67,6 +71,12 @@ export interface IDependencyProvider { * @returns The resolved name of the dependency. */ resolveDependencyFromReference(ref: string): string; + + /** + * Gets the name of the providers ecosystem. + * @returns The name of the providers ecosystem. + */ + getEcosystem(): string; } /** @@ -85,7 +95,15 @@ export class EcosystemDependencyResolver { * @returns The resolved name of the dependency. */ resolveDependencyFromReference(ref: string): string { - return ref.replace(`pkg:${this.ecosystem}/`, ''); + return ref.replace(`pkg:${ecosystemNameMappings[this.ecosystem]}/`, ''); + } + + /** + * Gets the name of the ecosystem this provider is configured for. + * @returns The name of the ecosystem. + */ + getEcosystem(): string { + return this.ecosystem; } } diff --git a/src/dependencyAnalysis/diagnostics.ts b/src/dependencyAnalysis/diagnostics.ts index 2f72ae96..0a60982d 100644 --- a/src/dependencyAnalysis/diagnostics.ts +++ b/src/dependencyAnalysis/diagnostics.ts @@ -11,7 +11,7 @@ import { IPositionedContext } from '../positionTypes'; import { executeComponentAnalysis, DependencyData } from './analysis'; import { Vulnerability } from '../vulnerability'; import { connection } from '../server'; -import { VERSION_PLACEHOLDER } from '../constants'; +import { VERSION_PLACEHOLDER, GRADLE } from '../constants'; import { clearCodeActionsMap, registerCodeAction, generateSwitchToRecommendedVersionAction } from '../codeActionHandler'; import { decodeUriPath } from '../utils'; import { AbstractDiagnosticsPipeline } from '../diagnosticsPipeline'; @@ -37,10 +37,11 @@ class DiagnosticsPipeline extends AbstractDiagnosticsPipeline { /** * Runs diagnostics on dependencies. * @param dependencies - A map containing dependency data by reference string. + * @param ecosystem - The name of the ecosystem in which dependencies are being analyzed. */ - runDiagnostics(dependencies: Map) { + runDiagnostics(dependencies: Map, ecosystem: string) { Object.entries(dependencies).map(([ref, dependencyData]) => { - const dependencyRef = ref.split('@')[0]; + const dependencyRef = ecosystem === GRADLE ? ref : ref.split('@')[0]; const dependency = this.dependencyMap.get(dependencyRef); if (dependency) { @@ -101,7 +102,8 @@ class DiagnosticsPipeline extends AbstractDiagnosticsPipeline { async function performDiagnostics(diagnosticFilePath: string, contents: string, provider: IDependencyProvider) { try { const dependencies = await provider.collect(contents); - const dependencyMap = new DependencyMap(dependencies); + const ecosystem = provider.getEcosystem(); + const dependencyMap = new DependencyMap(dependencies, ecosystem); const diagnosticsPipeline = new DiagnosticsPipeline(dependencyMap, diagnosticFilePath); diagnosticsPipeline.clearDiagnostics(); @@ -110,7 +112,7 @@ async function performDiagnostics(diagnosticFilePath: string, contents: string, clearCodeActionsMap(diagnosticFilePath); - diagnosticsPipeline.runDiagnostics(response.dependencies); + diagnosticsPipeline.runDiagnostics(response.dependencies, ecosystem); diagnosticsPipeline.reportDiagnostics(); } catch (error) { diff --git a/src/diagnosticsPipeline.ts b/src/diagnosticsPipeline.ts index e72f8273..bbb945d6 100644 --- a/src/diagnosticsPipeline.ts +++ b/src/diagnosticsPipeline.ts @@ -25,8 +25,9 @@ interface IDiagnosticsPipeline { /** * Runs diagnostics on dependencies. * @param artifact - A map containing artifact data. + * @param ecosystem - The name of the ecosystem to analyze dependencies in. */ - runDiagnostics(artifact: Map); + runDiagnostics(artifact: Map, ecosystem: string); } /** @@ -66,7 +67,7 @@ abstract class AbstractDiagnosticsPipeline implements IDiagnosticsPipeline }); } - abstract runDiagnostics(artifact: Map): void; + abstract runDiagnostics(artifact: Map, ecosystem: string): void; } export { AbstractDiagnosticsPipeline }; \ No newline at end of file diff --git a/src/fileHandler.ts b/src/fileHandler.ts index 3abccf17..15d06fc7 100644 --- a/src/fileHandler.ts +++ b/src/fileHandler.ts @@ -13,6 +13,7 @@ import { DependencyProvider as PackageJson } from './providers/package.json'; import { DependencyProvider as PomXml } from './providers/pom.xml'; import { DependencyProvider as GoMod } from './providers/go.mod'; import { DependencyProvider as RequirementsTxt } from './providers/requirements.txt'; +import { DependencyProvider as BuildGradle } from './providers/build.gradle'; import { ImageProvider as Docker } from './providers/docker'; /** @@ -120,6 +121,10 @@ files.on(EventStream.Diagnostics, '^requirements\\.txt$', (uri, contents) => { dependencyDiagnostics.performDiagnostics(uri, contents, new RequirementsTxt()); }); +files.on(EventStream.Diagnostics, '^build\\.gradle$', (uri, contents) => { + dependencyDiagnostics.performDiagnostics(uri, contents, new BuildGradle()); +}); + files.on(EventStream.Diagnostics, '^(Dockerfile|Containerfile)$', (uri, contents) => { imageDiagnostics.performDiagnostics(uri, contents, new Docker()); }); diff --git a/src/providers/build.gradle.ts b/src/providers/build.gradle.ts new file mode 100644 index 00000000..fddce36f --- /dev/null +++ b/src/providers/build.gradle.ts @@ -0,0 +1,268 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { VERSION_PLACEHOLDER, GRADLE } from '../constants'; +import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../dependencyAnalysis/collector'; + +/** + * Process entries found in the build.gradle file. + */ +export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { + + args: Map = new Map(); + + /** + * Regular expression for matching inline comments. + */ + COMMENT_REGEX: RegExp = /\/\*[\s\S]*?\*\//g; + + /** + * Regular expression for locating key-value pairs in a string with colons as separators. + */ + FIND_KEY_VALUE_PAIRS_WITH_COLON_REGEX: RegExp = /\b(\w+)\s*:\s*(['"])(.*?)\2/g; + + /** + * Regular expression for locating key-value pairs in a string with equals signs as separators. + */ + FIND_KEY_VALUE_PAIRS_WITH_EQUALS_REGEX: RegExp = /\b(\w+)\s*=\s*(['"])(.*?)\2/; + + /** + * Regular expression for matching key value pairs. + */ + SPLIT_KEY_VALUE_PAIRS_WITH_COLON_REGEX: RegExp = /\s*:\s*/; + + /** + * Regular expression for matching strings enclosed in double or single quotes. + */ + BETWEEN_QUOTES_REGEX: RegExp = /(['"])(.*?)\1/; + + /** + * Regular expression for matching open brackets in string. + */ + OPEN_BRACKETS_REGEX: RegExp = /{/g; + + /** + * Regular expression for matching close brackets in string. + */ + CLOSE_BRACKETS_REGEX: RegExp = /}/g; + + /** + * Name of scope holding manifest dependencies. + */ + DEPENDENCIES_SCOPE: string = 'dependencies'; + + /** + * Name of scope holding manifest arguments. + */ + ARGS_SCOPE: string = 'ext'; + + constructor() { + super(GRADLE); // set ecosystem to 'gradle' + } + + /** + * Parses the provided string as an array of lines. + * @param contents - The string content to parse into lines. + * @returns An array of strings representing lines from the provided content. + */ + private parseTxtDoc(contents: string): string[] { + return contents.split('\n'); + } + + /** + * Replaces placeholders in a string with values from an args map. + * @param str - The string containing placeholders. + * @returns The string with placeholders replaced by corresponding values from the args map. + * @private + */ + private replaceArgsInString(str: string): string { + this.args.forEach((value, key) => { + str = str.replace(`$\{${key}}`, value).replace(`$${key}`, value); + }); + return str; + } + + /** + * Parses a line from the file and extracts dependency information. + * @param line - The line to parse for dependency information. + * @param cleanLine - The line to parse for dependency information cleaned of comments. + * @param index - The index of the line in the file. + * @returns An IDependency object representing the parsed dependency or null if no dependency is found. + */ + private parseLine(line: string, cleanLine: string, index: number): IDependency | null { + const myClassObj = { group: '', name: '', version: '' }; + let depData: string; + let quoteUsed: string; + + const keyValuePairs = cleanLine.match(this.FIND_KEY_VALUE_PAIRS_WITH_COLON_REGEX); + if (keyValuePairs) { + // extract data from dependency in Map format + keyValuePairs.forEach(pair => { + const [key, value] = pair.split(this.SPLIT_KEY_VALUE_PAIRS_WITH_COLON_REGEX); + const match = value.match(this.BETWEEN_QUOTES_REGEX); + quoteUsed = match[1]; + const valueData = match[2]; + switch (key) { + case 'group': + myClassObj.group = valueData; + break; + case 'name': + myClassObj.name = valueData; + break; + case 'version': + myClassObj.version = valueData; + break; + } + }); + } else { + // extract data from dependency in String format + const match = cleanLine.match(this.BETWEEN_QUOTES_REGEX); + quoteUsed = match[1]; + depData = match[2]; + const depDataList = depData.split(':'); + myClassObj.group = depDataList[0]; + myClassObj.name = depDataList[1] || ''; + myClassObj.version = depDataList[2] || ''; + } + + // ignore dependencies missing minimal requirements + if (myClassObj.group === '' || myClassObj.name === '') {return null; } + + // determine dependency name + let depName: string = `${myClassObj.group}/${myClassObj.name}`; + if (depName.includes('$')) { + depName = this.replaceArgsInString(depName); + } + const dep = new Dependency ({ value: depName, position: { line: 0, column: 0 } }); + + // determine dependency version + const depVersion: string = myClassObj.version; + if (depVersion) { + dep.version = { value: depVersion, position: { line: index + 1, column: line.indexOf(depVersion) + 1 } }; + } else { + // if version is not specified, generate placeholder template + if (keyValuePairs) { + const quotedName = `${quoteUsed}${myClassObj.name}${quoteUsed}`; + dep.context = { value: `name: ${quotedName}, version: ${quoteUsed}${VERSION_PLACEHOLDER}${quoteUsed}`, range: { + start: { line: index, character: line.indexOf(`name: ${quotedName}`)}, + end: { line: index, character: line.indexOf(`name: ${quotedName}`) + `name: ${quotedName}`.length} + }, + }; + } else { + dep.context = { value: `${depData}:${VERSION_PLACEHOLDER}`, range: { + start: { line: index, character: line.indexOf(depData)}, + end: { line: index, character: line.indexOf(depData) + depData.length} + } + }; + } + } + return dep; + } + + /** + * Extracts dependencies from lines parsed from the file. + * @param lines - An array of strings representing lines from the file. + * @returns An array of IDependency objects representing extracted dependencies. + */ + private extractDependenciesFromLines(lines: string[]): IDependency[] { + let isSingleDependency: boolean = false; + let isDependencyBlock: boolean = false; + let isSingleArgument: boolean = false; + let isArgumentBlock: boolean = false; + let innerDepScopeBracketsCount: number = 0; + return lines.reduce((dependencies: IDependency[], line: string, index: number) => { + + const cleanLine = line.split('//')[0].replace(this.COMMENT_REGEX, '').trim(); // Remove comments + if (!cleanLine) { return dependencies; } // Skip empty lines + const parsedLine = cleanLine.includes('$') ? this.replaceArgsInString(cleanLine) : cleanLine; + const countOpenBrackets = (parsedLine.match(this.OPEN_BRACKETS_REGEX) || []).length; + const countCloseBrackets = (parsedLine.match(this.CLOSE_BRACKETS_REGEX) || []).length; + const updateInnerDepScopeBracketsCounter = () => { + innerDepScopeBracketsCount+=countOpenBrackets; + innerDepScopeBracketsCount-=countCloseBrackets; + }; + + if (isDependencyBlock) { + updateInnerDepScopeBracketsCounter(); + } + + if (isSingleDependency) { + if (parsedLine.startsWith('{')) { + updateInnerDepScopeBracketsCounter(); + isDependencyBlock = true; + } + isSingleDependency = false; + } + + if (parsedLine.includes(this.DEPENDENCIES_SCOPE)) { + updateInnerDepScopeBracketsCounter(); + + if (innerDepScopeBracketsCount > 0) { + isDependencyBlock = true; + } + + if (innerDepScopeBracketsCount === 0) { + isSingleDependency = true; + } + } + + if (isSingleDependency || isDependencyBlock) { + + if (innerDepScopeBracketsCount === 0) { + isDependencyBlock = false; + } + + if (!this.BETWEEN_QUOTES_REGEX.test(parsedLine)) { + return dependencies; + } + + const parsedDependency = this.parseLine(line, cleanLine, index); + if (parsedDependency) { + dependencies.push(parsedDependency); + } + return dependencies; + } + + if (isSingleArgument) { + if (parsedLine.startsWith('{')) { + isArgumentBlock = true; + } + isSingleArgument = false; + } + + if (parsedLine.includes(this.ARGS_SCOPE)) { + if (parsedLine.includes('{')) { + isArgumentBlock = true; + } else { + isSingleArgument = true; + } + } + + if (isSingleArgument || isArgumentBlock) { + if (parsedLine.includes('}')) { + isArgumentBlock = false; + } + + const argDataMatch = parsedLine.match(this.FIND_KEY_VALUE_PAIRS_WITH_EQUALS_REGEX); + if (argDataMatch) { + this.args.set(argDataMatch[1].trim(), argDataMatch[3].trim()); + } + } + + return dependencies; + }, []); + } + + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest content to collect dependencies from. + * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. + */ + async collect(contents: string): Promise { + const lines: string[] = this.parseTxtDoc(contents); + return this.extractDependenciesFromLines(lines); + } +} diff --git a/src/providers/go.mod.ts b/src/providers/go.mod.ts index a650611a..5ee0a32a 100644 --- a/src/providers/go.mod.ts +++ b/src/providers/go.mod.ts @@ -5,6 +5,7 @@ 'use strict'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../dependencyAnalysis/collector'; +import { GOLANG } from '../constants'; /* Please note :: There is an issue with the usage of semverRegex Node.js package in this code. * Often times it fails to recognize versions that contain an added suffix, usually including extra details such as a timestamp and a commit hash. @@ -27,7 +28,7 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I replacementMap: Map = new Map(); constructor() { - super('golang'); // set ecosystem to 'golang' + super(GOLANG); // set ecosystem to 'golang' } /** diff --git a/src/providers/package.json.ts b/src/providers/package.json.ts index 155cf3bb..df97e4cd 100644 --- a/src/providers/package.json.ts +++ b/src/providers/package.json.ts @@ -6,6 +6,7 @@ import jsonAst from 'json-to-ast'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../dependencyAnalysis/collector'; +import { NPM } from '../constants'; /** * Process entries found in the package.json file. @@ -14,7 +15,7 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I classes: string[] = ['dependencies']; constructor() { - super('npm'); // set ecosystem to 'npm' + super(NPM); // set ecosystem to 'npm' } /** diff --git a/src/providers/pom.xml.ts b/src/providers/pom.xml.ts index d8f0c2f0..71338d71 100644 --- a/src/providers/pom.xml.ts +++ b/src/providers/pom.xml.ts @@ -7,7 +7,7 @@ import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../dependencyAnalysis/collector'; import { parse, DocumentCstNode } from '@xml-tools/parser'; import { buildAst, accept, XMLElement, XMLDocument } from '@xml-tools/ast'; -import { VERSION_PLACEHOLDER } from '../constants'; +import { VERSION_PLACEHOLDER, MAVEN } from '../constants'; /** * Process entries found in the pom.xml file. @@ -15,7 +15,7 @@ import { VERSION_PLACEHOLDER } from '../constants'; export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { constructor() { - super('maven'); // set ecosystem to 'maven' + super(MAVEN); // set ecosystem to 'maven' } /** diff --git a/src/providers/requirements.txt.ts b/src/providers/requirements.txt.ts index 96f5e475..9f6769d5 100644 --- a/src/providers/requirements.txt.ts +++ b/src/providers/requirements.txt.ts @@ -4,6 +4,7 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; +import { PYPI } from '../constants'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../dependencyAnalysis/collector'; /** @@ -12,7 +13,7 @@ import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependen export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { constructor() { - super('pypi'); // set ecosystem to 'pypi' + super(PYPI); // set ecosystem to 'pypi' } /** diff --git a/test/codeActionHandler.test.ts b/test/codeActionHandler.test.ts index fc2329de..fd0903ea 100644 --- a/test/codeActionHandler.test.ts +++ b/test/codeActionHandler.test.ts @@ -210,7 +210,8 @@ describe('Code Action Handler tests', () => { kind: CodeActionKind.QuickFix, command: { title: 'Analytics Report', - command: 'mockStackAnalysisCommand' + command: 'mockStackAnalysisCommand', + arguments: ['', true] } } ] diff --git a/test/dependencyAnalysis/collector.test.ts b/test/dependencyAnalysis/collector.test.ts index 836d6a74..37afb546 100644 --- a/test/dependencyAnalysis/collector.test.ts +++ b/test/dependencyAnalysis/collector.test.ts @@ -3,7 +3,12 @@ import { expect } from 'chai'; import { Dependency, DependencyMap, getRange } from '../../src/dependencyAnalysis/collector'; -import { DependencyProvider } from '../../src/providers/pom.xml'; +import { DependencyProvider as PackageJson } from '../../src/providers/package.json'; +import { DependencyProvider as PomXml } from '../../src/providers/pom.xml'; +import { DependencyProvider as GoMod } from '../../src/providers/go.mod'; +import { DependencyProvider as RequirementsTxt } from '../../src/providers/requirements.txt'; +import { DependencyProvider as BuildGradle } from '../../src/providers/build.gradle'; +import * as constants from '../../src/constants'; describe('Dependency Analysis Collector tests', () => { @@ -15,7 +20,7 @@ describe('Dependency Analysis Collector tests', () => { it('should create map of dependecies', async () => { - const depMap = new DependencyMap(reqDeps); + const depMap = new DependencyMap(reqDeps, constants.MAVEN); expect(Object.fromEntries(depMap.mapper)).to.eql({ 'mockGroupId1/mockArtifact1': reqDeps[0], @@ -25,14 +30,14 @@ describe('Dependency Analysis Collector tests', () => { it('should create empty dependency map', async () => { - const depMap = new DependencyMap([]); + const depMap = new DependencyMap([], constants.MAVEN); expect(Object.keys(depMap.mapper).length).to.eql(0); }); it('should get dependency from dependency map', async () => { - const depMap = new DependencyMap(reqDeps); + const depMap = new DependencyMap(reqDeps, constants.MAVEN); expect(depMap.get(reqDeps[0].name.value)).to.eq(reqDeps[0]); expect(depMap.get(reqDeps[1].name.value)).to.eq(reqDeps[1]); @@ -57,11 +62,53 @@ describe('Dependency Analysis Collector tests', () => { expect(getRange(reqDeps[1])).to.eql(reqDeps[1].context.range); }); + it('should create map of dependecies for Gradle', async () => { + + const depMap = new DependencyMap(reqDeps, constants.GRADLE); + + expect(Object.fromEntries(depMap.mapper)).to.eql({ + 'mockGroupId1/mockArtifact1@mockVersion': reqDeps[0], + 'mockGroupId2/mockArtifact2': reqDeps[1] + }); + }); + it('should resolves a dependency reference in a specified ecosystem to its name and version string', async () => { - const mavenDependencyProvider = new DependencyProvider(); + const mavenDependencyProvider = new PomXml(); const resolvedRef = mavenDependencyProvider.resolveDependencyFromReference('pkg:maven/mockGroupId1/mockArtifact1@mockVersion1'); expect(resolvedRef).to.eq('mockGroupId1/mockArtifact1@mockVersion1'); }); + + it('should resolves a dependency reference in a specified ecosystem to its name and version string for Gradle', async () => { + const gradleDependencyProvider = new BuildGradle(); + + // Gradle references are marked as type 'maven' + const resolvedRef = gradleDependencyProvider.resolveDependencyFromReference('pkg:maven/mockGroupId1/mockArtifact1@mockVersion1'); + + expect(resolvedRef).to.eq('mockGroupId1/mockArtifact1@mockVersion1'); + }); + + it('should return the name of the providers ecosystem', async () => { + const npmDependencyProvider = new PackageJson(); + const mavenDependencyProvider = new PomXml(); + const golangDependencyProvider = new GoMod(); + const pythonDependencyProvider = new RequirementsTxt(); + const gradleDependencyProvider = new BuildGradle(); + + let ecosystem = npmDependencyProvider.getEcosystem(); + expect(ecosystem).to.eq(constants.NPM); + + ecosystem = mavenDependencyProvider.getEcosystem(); + expect(ecosystem).to.eq(constants.MAVEN); + + ecosystem = golangDependencyProvider.getEcosystem(); + expect(ecosystem).to.eq(constants.GOLANG); + + ecosystem = pythonDependencyProvider.getEcosystem(); + expect(ecosystem).to.eq(constants.PYPI); + + ecosystem = gradleDependencyProvider.getEcosystem(); + expect(ecosystem).to.eq(constants.GRADLE); + }); }) diff --git a/test/providers/build.gradle.test.ts b/test/providers/build.gradle.test.ts new file mode 100644 index 00000000..f913a16b --- /dev/null +++ b/test/providers/build.gradle.test.ts @@ -0,0 +1,347 @@ +'use strict'; + +import { expect } from 'chai'; + +import { DependencyProvider } from '../../src/providers/build.gradle'; + +describe('Gradle build.gradle parser tests', () => { + let dependencyProvider: DependencyProvider; + + beforeEach(() => { + dependencyProvider = new DependencyProvider(); + }); + + it('tests empty build.gradle', async () => { + const deps = await dependencyProvider.collect(``); + expect(deps).is.eql([]); + }); + + it('tests build.gradle with string notation dependencies', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "log4j:log4j:1.2.3" + implementation 'log4j:log4j:1.2.3' +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 11, column: 33}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 12, column: 33}} + } + ]); + }); + + it('tests build.gradle with map notation dependencies', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation group: "log4j", name: "log4j", version: "1.2.3" + implementation version: '1.2.3', name: 'log4j', group: 'log4j' +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 11, column: 61}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 12, column: 30}} + } + ]); + }); + + it('tests build.gradle with comments', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} +// a comment +dependencies { // a second comment + // implementation group: "log4j", name: "log4j", version: "1.2.3" + implementation version: "1.2.3", /* another comment */ name: "log4j", group: "log4j" // yet another comment + /* last comment */ +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 12, column: 30}} + } + ]); + }); + + it('tests build.gradle with empty lines', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + + +dependencies { + + + + implementation "log4j:log4j:1.2.3" + + +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 15, column: 33}} + } + ]); + }); + + it('tests build.gradle with spaces before and after package parameters', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "log4j:log4j:1.2.3" + implementation group: "log4j", name: "log4j", version:"1.2.3" +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 11, column: 43}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 12, column: 86}} + } + ]); + }); + + it('tests build.gradle with arguments', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +ext mockArg = 'mock' + +ext { nameArg = "log4j" } + +ext { + mockArg = 'mock'} + +ext { + versionArg = '1.2.3' + groupArg = 'log4j' +} + +ext +{ mockArg = 'mock' } + +ext +{ + mockArg = 'mock' +} +repositories { + mavenCentral() +} + +dependencies { + implementation "\${groupArg}:\${nameArg}:\${versionArg}" + implementation group: "log4j", name: "\${nameArg}", version: "\${versionArg}" +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '${versionArg}', position: {line: 30, column: 44}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '${versionArg}', position: {line: 31, column: 66}} + } + ]); + }); + + it('tests build.gradle with single line dependency declarations', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { implementation "log4j:log4j:1.2.3" } +dependencies { implementation group: "log4j", name: "log4j", version: "1.2.3" } + + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 10, column: 44}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 11, column: 72}} + } + ]); + }); + + it('tests build.gradle with muliple dependency declarations', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies implementation "log4j:log4j:1.2.3" + +dependencies { implementation "log4j:log4j:1.2.3" } + +dependencies { + implementation 'log4j:log4j:1.2.3'} + +dependencies { + implementation group: "log4j", name: "log4j", version: "1.2.3" +} + +dependencies +{ implementation group: 'log4j', name: 'log4j', version: '1.2.3' } +dependencies +{ + implementation group: 'log4j', name: 'log4j', version: '1.2.3' +} + + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 10, column: 42}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 12, column: 44}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 15, column: 33}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 18, column: 61}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 22, column: 59}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 25, column: 61}} + } + ]); + }); + + it('tests build.gradle with special dependency declarations', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation('commons-codec:commons-codec') { + version { + strictly '1.9' + } + } + implementation('commons-beanutils:commons-beanutils:1.9.4') { + exclude group: 'commons-collections', module: 'commons-collections' + } + implementation project(":module_a") + api libs.io.quarkus.quarkus.vertx.http + implementation "log4j:log4j:1.2.3" +} + `); + expect(deps).is.eql([ + { + name: {value: 'commons-codec/commons-codec', position: {line: 0, column: 0}}, + context: {value: 'commons-codec:commons-codec:__VERSION__', range: {start: {line: 10, character: 20}, end: {line: 10, character: 47}}} + }, + { + name: {value: 'commons-beanutils/commons-beanutils', position: {line: 0, column: 0}}, + version: {value: '1.9.4', position: {line: 16, column: 57}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + version: {value: '1.2.3', position: {line: 21, column: 33}} + } + ]); + }); + + it('tests build.gradle dependencies with missing version parameter', async () => { + const deps = await dependencyProvider.collect(` +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "log4j:log4j" + implementation group: "log4j", name: "log4j" +} + `); + expect(deps).is.eql([ + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + context: {value: 'log4j:log4j:__VERSION__', range: {start: {line: 10, character: 20}, end: {line: 10, character: 31}}} + }, + { + name: {value: 'log4j/log4j', position: {line: 0, column: 0}}, + context: {value: 'name: "log4j", version: "__VERSION__"', range: {start: {line: 11, character: 35}, end: {line: 11, character: 48}}} + } + ]); + }); +});