From 3fa79c1d18bd2cb95e0f8df732ca87436b5d98fc Mon Sep 17 00:00:00 2001 From: Stephen Carter Date: Thu, 8 Feb 2024 17:19:44 -0500 Subject: [PATCH] @W-14980290@: Implement Pmd7CommandInfo to allow us to call pmd7 jars --- src/Constants.ts | 2 + src/lib/Display.ts | 14 ++- src/lib/actions/RuleDescribeAction.ts | 24 ++-- src/lib/cpd/CpdEngine.ts | 8 +- src/lib/pmd/PmdCommandInfo.ts | 32 ++++- src/types.d.ts | 1 + test/commands/scanner/rule/describe.test.ts | 2 +- test/lib/FakeDisplay.ts | 11 ++ test/lib/actions/RuleDescribeAction.test.ts | 69 +++++++++++ test/lib/actions/RuleListAction.test.ts | 72 ++++++++++++ test/lib/actions/RunAction.test.ts | 124 ++++++++++++++++++++ test/lib/actions/fakes.ts | 41 +++++++ test/lib/output/ResultsFormatting.test.ts | 51 +++++--- test/lib/pmd/PmdEngine.test.ts | 24 +++- 14 files changed, 442 insertions(+), 33 deletions(-) create mode 100644 test/lib/actions/RuleDescribeAction.test.ts create mode 100644 test/lib/actions/RuleListAction.test.ts create mode 100644 test/lib/actions/RunAction.test.ts create mode 100644 test/lib/actions/fakes.ts diff --git a/src/Constants.ts b/src/Constants.ts index f239b9e8e..d0f86b934 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -2,6 +2,7 @@ import os = require('os'); import path = require('path'); export const PMD6_VERSION = '6.55.0'; +export const PMD7_VERSION = '7.0.0-rc4'; export const PMD_APPEXCHANGE_RULES_VERSION = '0.12'; export const SFGE_VERSION = '1.0.1-pilot'; export const DEFAULT_SCANNER_PATH = path.join(os.homedir(), '.sfdx-scanner'); @@ -133,6 +134,7 @@ export enum Severity { // Here, current dir __dirname = /sfdx-scanner/src export const PMD6_LIB = path.join(__dirname, '..', 'dist', 'pmd', 'lib'); +export const PMD7_LIB = path.join(__dirname, '..', 'dist', 'pmd7', 'lib'); // Here, current dir __dirname = /sfdx-scanner/src export const APPEXCHANGE_PMD_LIB = path.join(__dirname, '..', 'pmd-appexchange', 'lib'); diff --git a/src/lib/Display.ts b/src/lib/Display.ts index b42ed9ed2..e12fe449f 100644 --- a/src/lib/Display.ts +++ b/src/lib/Display.ts @@ -1,5 +1,6 @@ import {Spinner} from "@salesforce/sf-plugins-core"; import {Ux} from "@salesforce/sf-plugins-core/lib/ux"; +import {AnyJson} from "@salesforce/ts-types"; export interface Display { /** @@ -27,6 +28,11 @@ export interface Display { */ displayTable(data: R[], columns: Ux.Table.Columns): void; + /** + * Output object to stdout only if the "--json" flag is not present. + */ + displayStyledObject(obj: AnyJson): void; + /** * Display a message as a warning. */ @@ -91,6 +97,10 @@ export class UxDisplay implements Display { this.displayable.table(data, columns); } + public displayStyledObject(obj: AnyJson): void { + this.displayable.styledObject(obj); + } + public displayWarning(msg: string): void { this.displayable.warn(msg); } @@ -126,13 +136,15 @@ export interface Displayable { // Display an error or message as a warning. [Implemented by Command] warn(input: string): void; - // Simplified prompt for single-question confirmation. Times out and throws after 10s. [Implemented by SfCommand] confirm(message: string): Promise; // Output stylized header to stdout only when "--json" flag is not present. [Implemented by SfCommand] styledHeader(headerText: string): void; + // Output stylized object to stdout only when "--json" flag is not present. [Implemented by SfCommand] + styledObject(obj: AnyJson): void; + // Output table to stdout only when "--json" flag is not present. [Implemented by SfCommand] table(data: R[], columns: Ux.Table.Columns, options?: Ux.Table.Options): void; } diff --git a/src/lib/actions/RuleDescribeAction.ts b/src/lib/actions/RuleDescribeAction.ts index e8f87cff0..6221e9727 100644 --- a/src/lib/actions/RuleDescribeAction.ts +++ b/src/lib/actions/RuleDescribeAction.ts @@ -7,7 +7,6 @@ import {BundleName, getMessage} from "../../MessageCatalog"; import {deepCopy} from "../util/Utils"; import Dfa from "../../commands/scanner/run/dfa"; import Run from "../../commands/scanner/run"; -import {Ux} from "@salesforce/sf-plugins-core"; import {Display} from "../Display"; import {RuleFilterFactory} from "../RuleFilterFactory"; @@ -34,8 +33,6 @@ export class RuleDescribeAction implements Action { } public async run(inputs: Inputs): Promise { - const jsonEnabled: boolean = inputs.json as boolean; - const ruleFilters: RuleFilter[] = this.ruleFilterFactory.createRuleFilters(inputs); // TODO: Inject RuleManager as a dependency to improve testability by removing coupling to runtime implementation @@ -52,11 +49,11 @@ export class RuleDescribeAction implements Action { this.display.displayWarning(msg); rules.forEach((rule, idx) => { this.display.displayStyledHeader(`Rule #${idx + 1}`); - this.displayStyledRule(rule, jsonEnabled); + this.displayStyledRule(rule); }); } else { // If there's exactly one rule, we don't need to do anything special, and can just log the rule. - this.displayStyledRule(rules[0], jsonEnabled); + this.displayStyledRule(rules[0]); } // We need to return something for when the --json flag is used, so we'll just return the list of rules. return deepCopy(rules); @@ -87,9 +84,18 @@ export class RuleDescribeAction implements Action { }); } - private displayStyledRule(rule: DescribeStyledRule, jsonEnabled: boolean): void { - // TODO: We should remove this instantiation of new Ux in favor of possibly a new method on Display - new Ux({jsonEnabled: jsonEnabled}) - .styledObject(rule, ['name', 'engine', 'runWith', 'isPilot', 'enabled', 'categories', 'rulesets', 'languages', 'description', 'message']); + private displayStyledRule(rule: DescribeStyledRule): void { + this.display.displayStyledObject({ + name: rule.name, + engine: rule.engine, + runWith: rule.runWith, + isPilot: rule.isPilot, + enabled: rule.enabled, + categories: rule.categories, + rulesets: rule.rulesets, + languages: rule.languages, + description: rule.description, + message: rule.message + }) } } diff --git a/src/lib/cpd/CpdEngine.ts b/src/lib/cpd/CpdEngine.ts index 327c5019d..8688e35aa 100644 --- a/src/lib/cpd/CpdEngine.ts +++ b/src/lib/cpd/CpdEngine.ts @@ -216,10 +216,10 @@ export class CpdEngine extends AbstractRuleEngine { for (const occ of occurences) { // create a violation for each occurence of the code fragment const violation: RuleViolation = { - line: occ.attributes.line as number, - column: occ.attributes.column as number, - endLine: occ.attributes.endline as number, - endColumn: occ.attributes.endcolumn as number, + line: Number(occ.attributes.line), + column: Number(occ.attributes.column), + endLine: Number(occ.attributes.endline), + endColumn: Number(occ.attributes.endcolumn), ruleName: CpdRuleName, severity: CpdViolationSeverity, message: getMessage(BundleName.CpdEngine, "CpdViolationMessage", [codeFragmentID, occCount, occurences.length, duplication.attributes.lines, duplication.attributes.tokens]), diff --git a/src/lib/pmd/PmdCommandInfo.ts b/src/lib/pmd/PmdCommandInfo.ts index 5a3017343..a5aaa0610 100644 --- a/src/lib/pmd/PmdCommandInfo.ts +++ b/src/lib/pmd/PmdCommandInfo.ts @@ -1,8 +1,9 @@ -import {PMD6_LIB, PMD6_VERSION} from "../../Constants"; +import {PMD6_LIB, PMD6_VERSION, PMD7_LIB, PMD7_VERSION} from "../../Constants"; import * as path from 'path'; const PMD6_MAIN_CLASS = 'net.sourceforge.pmd.PMD'; const CPD6_MAIN_CLASS = 'net.sourceforge.pmd.cpd.CPD'; +const PMD7_CLI_CLASS = 'net.sourceforge.pmd.cli.PmdCli'; const HEAP_SIZE = '-Xmx1024m'; export interface PmdCommandInfo { @@ -40,6 +41,33 @@ export class Pmd6CommandInfo implements PmdCommandInfo { constructJavaCommandArgsForCpd(fileList: string, minimumTokens: number, language: string): string[] { const classpath = `${PMD6_LIB}/*`; return ['-cp', classpath, HEAP_SIZE, CPD6_MAIN_CLASS, '--filelist', fileList, - '--format', 'xml', '--minimum-tokens', String(minimumTokens), '--language', language]; + '--format', 'xml', '--minimum-tokens', minimumTokens.toString(), '--language', language]; + } +} + +export class Pmd7CommandInfo implements PmdCommandInfo { + getVersion(): string { + return PMD7_VERSION; + } + + getJarPathForLanguage(language: string): string { + return path.join(PMD7_LIB, `pmd-${language}-${this.getVersion()}.jar`); + } + + constructJavaCommandArgsForPmd(fileList: string, classPathsForExternalRules: string[], rulesets: string): string[] { + const classpath = classPathsForExternalRules.concat([`${PMD7_LIB}/*`]).join(path.delimiter); + const args = ['-cp', classpath, HEAP_SIZE, PMD7_CLI_CLASS, 'check', '--file-list', fileList, + '--format', 'xml']; + if (rulesets.length > 0) { + args.push('--rulesets', rulesets); + } + return args; + } + + constructJavaCommandArgsForCpd(fileList: string, minimumTokens: number, language: string): string[] { + const classpath = `${PMD7_LIB}/*`; + const resolvedLanguage = language === 'visualforce' ? 'vf' : language; + return ['-cp', classpath, HEAP_SIZE, PMD7_CLI_CLASS, 'cpd', '--file-list', fileList, '--format', 'xml', + '--minimum-tokens', minimumTokens.toString(), '--language', resolvedLanguage, '--skip-lexical-errors']; } } diff --git a/src/types.d.ts b/src/types.d.ts index fa9a51219..21d9da770 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -18,6 +18,7 @@ export type Rule = { // be OR'd together in this property. defaultConfig?: ESRuleConfigValue; url?: string; + message?: string; } export type TelemetryData = { diff --git a/test/commands/scanner/rule/describe.test.ts b/test/commands/scanner/rule/describe.test.ts index 15cd92d0d..cdb914abe 100644 --- a/test/commands/scanner/rule/describe.test.ts +++ b/test/commands/scanner/rule/describe.test.ts @@ -60,7 +60,7 @@ describe('scanner rule describe', () => { expect(ctx.stderr.toLowerCase()).to.contain(`WARNING: ${formattedWarning}`.toLowerCase(), 'Warning message should be formatted correctly'); // Next, verify that there are rule descriptions that are distinctly identified. - const regex = /=== Rule #1\n\nname:\s+constructor-super(.*\n)*=== Rule #2\n\nname:\s+constructor-super(.*\n)*=== Rule #3\n\nname:\s+constructor-super/g; + const regex = /=== Rule #1\n(.*\n)*name:\s+constructor-super(.*\n)*=== Rule #2\n(.*\n)*name:\s+constructor-super(.*\n)*=== Rule #3\n(.*\n)*name:\s+constructor-super/g; expect(ctx.stdout).to.match(regex, 'Output should contain three rules named constructor-super for each eslint based engine'); }); diff --git a/test/lib/FakeDisplay.ts b/test/lib/FakeDisplay.ts index 3de33c141..b9ab12444 100644 --- a/test/lib/FakeDisplay.ts +++ b/test/lib/FakeDisplay.ts @@ -1,11 +1,13 @@ import {Display} from "../../src/lib/Display"; import {Ux} from "@salesforce/sf-plugins-core"; +import {AnyJson} from "@salesforce/ts-types"; export class FakeDisplay implements Display { private outputs: string[] = []; private confirmationPromptResponse: boolean = true; private lastTableColumns: Ux.Table.Columns; private lastTableData: Ux.Table.Data[]; + private lastStyledObject: AnyJson; public getOutputArray(): string[] { return this.outputs; @@ -27,6 +29,10 @@ export class FakeDisplay implements Display { return this.lastTableData; } + public getLastStyledObject(): AnyJson { + return this.lastStyledObject; + } + displayConfirmationPrompt(msg: string): Promise { this.outputs.push(msg); @@ -59,6 +65,11 @@ export class FakeDisplay implements Display { this.outputs.push("[Table][" + JSON.stringify(columns) + "]: " + JSON.stringify(data)); } + displayStyledObject(obj: AnyJson): void { + this.lastStyledObject = obj; + this.outputs.push(JSON.stringify(obj)) + } + spinnerStart(msg: string, status?: string): void { const statusText = status ? "[" + status + "]" : ""; this.outputs.push("[SpinnerStart]" + statusText + ": " + msg) diff --git a/test/lib/actions/RuleDescribeAction.test.ts b/test/lib/actions/RuleDescribeAction.test.ts new file mode 100644 index 000000000..23f5b508d --- /dev/null +++ b/test/lib/actions/RuleDescribeAction.test.ts @@ -0,0 +1,69 @@ +import {FakeDisplay} from "../FakeDisplay"; +import {initContainer} from "../../../src/ioc.config"; +import {RuleFilterFactoryImpl} from "../../../src/lib/RuleFilterFactory"; +import {Pmd7CommandInfo, PmdCommandInfo} from "../../../src/lib/pmd/PmdCommandInfo"; +import {Controller} from "../../../src/Controller"; +import {after} from "mocha"; +import {Inputs} from "../../../src/types"; +import {expect} from "chai"; +import {RuleDescribeAction} from "../../../src/lib/actions/RuleDescribeAction"; +import {AnyJson} from "@salesforce/ts-types"; + +describe("Tests for RuleDescribeAction", () => { + let display: FakeDisplay; + let ruleDescribeAction: RuleDescribeAction; + before(() => { + initContainer(); + }); + beforeEach(() => { + display = new FakeDisplay(); + ruleDescribeAction = new RuleDescribeAction(display, new RuleFilterFactoryImpl()); + }); + + describe("Tests to confirm that PMD7 binary files are invoked when choosing PMD7 with pmd engine", () => { + + // TODO: Soon we will have an input flag to control this. Once we do, we can update this to use that instead + const originalPmdCommandInfo: PmdCommandInfo = Controller.getActivePmdCommandInfo() + before(() => { + Controller.setActivePmdCommandInfo(new Pmd7CommandInfo()); + }); + after(() => { + Controller.setActivePmdCommandInfo(originalPmdCommandInfo); + }); + + it("When using PMD7, the rule description for a pmd rule should give correct info from PMD 7", async () => { + const inputs: Inputs = { + rulename: 'ApexCRUDViolation' + } + await ruleDescribeAction.run(inputs); + + const rule: AnyJson = display.getLastStyledObject(); + expect(rule['name']).to.equal('ApexCRUDViolation'); + expect(rule['engine']).to.equal('pmd'); + expect(rule['isPilot']).to.equal(false); + expect(rule['enabled']).to.equal(true); + expect(rule['categories']).to.deep.equal(['Security']); + expect(rule['rulesets']).to.contain('quickstart'); + expect(rule['languages']).to.deep.equal(['apex']); + expect(rule['description']).to.have.length.greaterThan(0); + expect(rule['message']).to.have.length.greaterThan(0); + }) + + it("When using PMD7, the rule description for a cpd rule should give back correct info from PMD 7", async () => { + const inputs: Inputs = { + rulename: 'copy-paste-detected' + } + await ruleDescribeAction.run(inputs); + + const rule: AnyJson = display.getLastStyledObject(); + expect(rule['name']).to.equal('copy-paste-detected'); + expect(rule['engine']).to.equal('cpd'); + expect(rule['isPilot']).to.equal(false); + expect(rule['enabled']).to.equal(false); + expect(rule['categories']).to.deep.equal(['Copy/Paste Detected']); + expect(rule['rulesets']).to.deep.equal([]); + expect(rule['languages']).to.deep.equal(['apex', 'java', 'visualforce', 'xml']); + expect(rule['description']).to.have.length.greaterThan(0); + }); + }); +}); diff --git a/test/lib/actions/RuleListAction.test.ts b/test/lib/actions/RuleListAction.test.ts new file mode 100644 index 000000000..c25849761 --- /dev/null +++ b/test/lib/actions/RuleListAction.test.ts @@ -0,0 +1,72 @@ +import {FakeDisplay} from "../FakeDisplay"; +import {initContainer} from "../../../src/ioc.config"; +import {RuleFilterFactoryImpl} from "../../../src/lib/RuleFilterFactory"; +import {RuleListAction} from "../../../src/lib/actions/RuleListAction"; +import {Pmd7CommandInfo, PmdCommandInfo} from "../../../src/lib/pmd/PmdCommandInfo"; +import {Controller} from "../../../src/Controller"; +import {after} from "mocha"; +import {Inputs} from "../../../src/types"; +import {expect} from "chai"; +import {Ux} from "@salesforce/sf-plugins-core"; +import {PMD7_LIB} from "../../../src/Constants"; + +describe("Tests for RuleListAction", () => { + let display: FakeDisplay; + let ruleListAction: RuleListAction; + before(() => { + initContainer(); + }); + beforeEach(() => { + display = new FakeDisplay(); + ruleListAction = new RuleListAction(display, new RuleFilterFactoryImpl()); + }); + + describe("Tests to confirm that PMD7 binary files are invoked when choosing PMD7", () => { + + // TODO: Soon we will have an input flag to control this. Once we do, we can update this to use that instead + const originalPmdCommandInfo: PmdCommandInfo = Controller.getActivePmdCommandInfo() + before(() => { + Controller.setActivePmdCommandInfo(new Pmd7CommandInfo()); + }); + after(() => { + Controller.setActivePmdCommandInfo(originalPmdCommandInfo); + }); + + it("When using PMD7, the rule list for the pmd engine should give back rules for PMD 7", async () => { + const inputs: Inputs = { + engine: ['pmd'] + } + await ruleListAction.run(inputs); + + let tableData: Ux.Table.Data[] = display.getLastTableData(); + expect(tableData).to.have.length(67); + for (const rowData of tableData) { + expect(rowData.engine).to.equal("pmd"); + expect(rowData.sourcepackage).to.contain(PMD7_LIB); + expect(rowData.name).to.have.length.greaterThan(0); + expect(rowData.categories).to.have.length.greaterThan(0); + expect(rowData.isDfa).to.equal(false); + expect(rowData.isPilot).to.equal(false); + expect(rowData.languages).to.have.length.greaterThan(0); + } + }) + + it("When using PMD7, the rule list for the cpd engine should give back the copy-paste-detected rule", async () => { + const inputs: Inputs = { + engine: ['cpd'] + } + await ruleListAction.run(inputs); + + let tableData: Ux.Table.Data[] = display.getLastTableData(); + expect(tableData).to.have.length(1); + expect(tableData[0].engine).to.equal("cpd"); + expect(tableData[0].sourcepackage).to.equal("cpd"); + expect(tableData[0].name).to.equal("copy-paste-detected"); + expect(tableData[0].categories).to.deep.equal(["Copy/Paste Detected"]); + expect(tableData[0].rulesets).to.deep.equal([]); + expect(tableData[0].isDfa).to.equal(false); + expect(tableData[0].isPilot).to.equal(false); + expect(tableData[0].languages).to.deep.equal(['apex', 'java', 'visualforce', 'xml']); + }); + }); +}); diff --git a/test/lib/actions/RunAction.test.ts b/test/lib/actions/RunAction.test.ts new file mode 100644 index 000000000..4fca40f42 --- /dev/null +++ b/test/lib/actions/RunAction.test.ts @@ -0,0 +1,124 @@ +import {InputProcessor, InputProcessorImpl} from "../../../src/lib/InputProcessor"; +import {RuleFilterFactoryImpl} from "../../../src/lib/RuleFilterFactory"; +import {RunEngineOptionsFactory} from "../../../src/lib/EngineOptionsFactory"; +import {RunAction} from "../../../src/lib/actions/RunAction"; +import {FakeDisplay} from "../FakeDisplay"; +import {Logger} from "@salesforce/core"; +import {Inputs, PathlessRuleViolation, RuleResult} from "../../../src/types"; +import * as path from "path"; +import {initContainer} from '../../../src/ioc.config'; +import {expect} from "chai"; +import {Pmd7CommandInfo, PmdCommandInfo} from "../../../src/lib/pmd/PmdCommandInfo"; +import {Controller} from "../../../src/Controller"; +import {after} from "mocha"; +import {Results} from "../../../src/lib/output/Results"; +import {PMD6_VERSION, PMD7_VERSION} from "../../../src/Constants"; +import {FakeResultsProcessorFactory, RawResultsProcessor} from "./fakes"; + +const codeFixturesDir = path.join(__dirname, '..', '..', 'code-fixtures'); +const pathToSomeTestClass = path.join(codeFixturesDir, 'apex', 'SomeTestClass.cls'); +const pathToCodeForCpd = path.join(codeFixturesDir, 'cpd'); + + +describe("Tests for RunAction", () => { + let display: FakeDisplay; + let resultsProcessor: RawResultsProcessor; + let runAction: RunAction; + before(() => { + initContainer(); + }); + beforeEach(() => { + display = new FakeDisplay(); + resultsProcessor = new RawResultsProcessor(); + + const inputProcessor: InputProcessor = new InputProcessorImpl("2.11.8", display); + runAction = new RunAction( + Logger.childFromRoot("forTesting"), + display, + inputProcessor, + new RuleFilterFactoryImpl(), + new RunEngineOptionsFactory(inputProcessor), + new FakeResultsProcessorFactory(resultsProcessor)); + }); + + describe("Tests to confirm that PMD7 binary files are invoked when choosing PMD7", () => { + + // TODO: Soon we will have an input flag to control this. Once we do, we can update this to use that instead + const originalPmdCommandInfo: PmdCommandInfo = Controller.getActivePmdCommandInfo() + before(() => { + Controller.setActivePmdCommandInfo(new Pmd7CommandInfo()); + }); + after(() => { + Controller.setActivePmdCommandInfo(originalPmdCommandInfo); + }); + + it("When using PMD7, the pmd engine actually uses PMD7 instead of PMD6", async () => { + const inputs: Inputs = { + target: [pathToSomeTestClass], + engine: ['pmd'], + 'normalize-severity': true + } + await runAction.run(inputs); + + const results: Results = resultsProcessor.getResults(); + expect(results.getExecutedEngines().size).to.equal(1); + expect(results.getExecutedEngines()).to.contain('pmd'); + const ruleResults: RuleResult[] = results.getRuleResults(); + expect(ruleResults).to.have.length(1); + expect(ruleResults[0].violations).to.have.length(8); + for (let violation of ruleResults[0].violations) { + violation = violation as PathlessRuleViolation; + + // Unfortunately, there isn't an easy way to detect that we are using PMD 7 binaries other than checking + // that the violation urls contain version 7 information instead of version 6. + expect(violation.url).to.contain(PMD7_VERSION); + expect(violation.url).not.to.contain(PMD6_VERSION); + + // Other sanity checks to make the fields are filled in + expect(violation.ruleName).to.have.length.greaterThan(0); + expect(violation.category).to.have.length.greaterThan(0); + expect(violation.line).to.be.greaterThan(0); + expect(violation.column).to.be.greaterThan(0); + expect(violation.message).to.have.length.greaterThan(0); + expect(violation.severity).to.be.greaterThanOrEqual(3); + expect(violation.normalizedSeverity).to.equal(3); + } + }); + + it("When using PMD7, the cpd engine actually uses PMD7 instead of PMD6", async () => { + const inputs: Inputs = { + target: [pathToCodeForCpd], + engine: ['cpd'], + 'normalize-severity': true + } + await runAction.run(inputs); + + const results: Results = resultsProcessor.getResults(); + expect(results.getExecutedEngines().size).to.equal(1); + expect(results.getExecutedEngines()).to.contain('cpd'); + const ruleResults: RuleResult[] = results.getRuleResults(); + expect(ruleResults).to.have.length(2); + expect(ruleResults[0].violations).to.have.length(1); + expect(ruleResults[1].violations).to.have.length(1); + const violation1: PathlessRuleViolation = ruleResults[0].violations[0] as PathlessRuleViolation; + const violation2: PathlessRuleViolation = ruleResults[1].violations[0] as PathlessRuleViolation; + + for (let violation of [violation1, violation2]) { + + // Unfortunately, there isn't an easy way to detect that we are using PMD 7 binaries. + // The best we can do is check for 'latest' in the url. + expect(violation.url).to.contain('latest'); + expect(violation.url).not.to.contain(PMD6_VERSION); + + // Other sanity checks to make the fields are filled in + expect(violation.ruleName).to.have.length.greaterThan(0); + expect(violation.category).to.have.length.greaterThan(0); + expect(violation.line).to.be.greaterThan(0); + expect(violation.column).to.be.greaterThan(0); + expect(violation.message).to.have.length.greaterThan(0); + expect(violation.severity).to.be.greaterThanOrEqual(3); + expect(violation.normalizedSeverity).to.equal(3); + } + }); + }); +}); diff --git a/test/lib/actions/fakes.ts b/test/lib/actions/fakes.ts new file mode 100644 index 000000000..e198733b5 --- /dev/null +++ b/test/lib/actions/fakes.ts @@ -0,0 +1,41 @@ +import {ResultsProcessorFactory} from "../../../src/lib/output/ResultsProcessorFactory"; +import {ResultsProcessor} from "../../../src/lib/output/ResultsProcessor"; +import {Display} from "../../../src/lib/Display"; +import {RunOutputOptions} from "../../../src/lib/output/RunResultsProcessor"; +import {JsonReturnValueHolder} from "../../../src/lib/output/JsonReturnValueHolder"; +import {Results} from "../../../src/lib/output/Results"; + + + +/** + * This fake does zero processing, but instead gives you the ability to access the raw results that were to be processed + */ +export class RawResultsProcessor implements ResultsProcessor { + private results: Results; + + processResults(results: Results): Promise { + this.results = results; + return Promise.resolve(); + } + + getResults(): Results { + return this.results; + } +} + + + +/** + * This fake just passes back whatever results processor you pass in. + */ +export class FakeResultsProcessorFactory implements ResultsProcessorFactory { + private readonly resultsProcessor: ResultsProcessor; + + constructor(resultsProcessor: ResultsProcessor) { + this.resultsProcessor = resultsProcessor + } + + createResultsProcessor(_d: Display, _r: RunOutputOptions, _j: JsonReturnValueHolder): ResultsProcessor { + return this.resultsProcessor + } +} diff --git a/test/lib/output/ResultsFormatting.test.ts b/test/lib/output/ResultsFormatting.test.ts index 0a5763b71..e828093b7 100644 --- a/test/lib/output/ResultsFormatting.test.ts +++ b/test/lib/output/ResultsFormatting.test.ts @@ -10,6 +10,9 @@ import { PathlessEngineFilters, ENGINE, PMD6_VERSION, SFGE_VERSION } from '../.. import { fail } from 'assert'; import {Results, RunResults} from "../../../src/lib/output/Results"; import { OutputFormat } from '../../../src/lib/output/OutputFormat'; +import {Controller} from "../../../lib/Controller"; +import {Pmd7CommandInfo, PmdCommandInfo} from "../../../lib/lib/pmd/PmdCommandInfo"; +import {PMD7_VERSION} from "../../../lib/Constants"; const sampleFile1 = path.join('Users', 'SomeUser', 'samples', 'sample-file1.js'); const sampleFile2 = path.join('Users', 'SomeUser', 'samples', 'sample-file2.js'); @@ -303,6 +306,22 @@ const retireJsVerboseViolations: RuleResult[] = [ } ]; +function createSampleRuleResultsForEngine(engine: string): RuleResult[] { + return [{ + engine: engine, + fileName: sampleFile1, + violations: [{ + "line": 2, + "column": 11, + "severity": 2, + "message": "A generic message", + "ruleName": "rule-name", + "category": "category-name", + "url": "https://some/url.org" + }] + }]; +} + function isString(x: string | {columns; rows}): x is string { return typeof x === 'string'; } @@ -610,7 +629,6 @@ describe('Results Formatting', () => { } it ('Happy Path - pathless rules', async () => { - const results: Results = new RunResults(allFakePathlessRuleResults, new Set(['eslint', 'pmd'])); const formattedOutput: FormattedOutput = await results.toFormattedOutput(OutputFormat.SARIF, false); const minSev: number = results.getMinSev(); @@ -744,20 +762,7 @@ describe('Results Formatting', () => { it('Handles all pathless engines', async () => { const allEngines = PathlessEngineFilters.map(engine => engine.valueOf()); for (const engine of allEngines) { - const ruleResults: RuleResult[] = [{ - engine: engine, - fileName: sampleFile1, - violations: [{ - "line": 2, - "column": 11, - "severity": 2, - "message": "A generic message", - "ruleName": "rule-name", - "category": "category-name", - "url": "https://some/url.org" - }] - }]; - const results: Results = new RunResults(ruleResults, new Set([engine])); + const results: Results = new RunResults(createSampleRuleResultsForEngine(engine), new Set([engine])); await results.toFormattedOutput(OutputFormat.SARIF, false); // should throw an error if the engine was not handled } @@ -785,6 +790,22 @@ describe('Results Formatting', () => { } } }); + + it ('Switching to PMD7 is reflected in sarif output for pmd and cpd engines', async () => { + const originalPmdCommandInfo: PmdCommandInfo = Controller.getActivePmdCommandInfo() + try { + Controller.setActivePmdCommandInfo(new Pmd7CommandInfo()); + for (const engine of ['pmd', 'cpd']) { + const results: Results = new RunResults(createSampleRuleResultsForEngine(engine), new Set([engine])); + const formattedOutput: FormattedOutput = await results.toFormattedOutput(OutputFormat.SARIF, false); + const sarifResults: unknown[] = JSON.parse(formattedOutput as string); + expect(sarifResults['runs']).to.have.lengthOf(1); + expect(sarifResults['runs'][0]['tool']['driver']['version']).to.equal(PMD7_VERSION); + } + } finally { + Controller.setActivePmdCommandInfo(originalPmdCommandInfo); + } + }) }); describe('Output Format: JSON', () => { diff --git a/test/lib/pmd/PmdEngine.test.ts b/test/lib/pmd/PmdEngine.test.ts index 8ce58784f..f9e03a81b 100644 --- a/test/lib/pmd/PmdEngine.test.ts +++ b/test/lib/pmd/PmdEngine.test.ts @@ -7,12 +7,14 @@ import Sinon = require('sinon'); import {PmdEngine, _PmdRuleMapper} from '../../../src/lib/pmd/PmdEngine' import {uxEvents, EVENTS} from '../../../src/lib/ScannerEvents'; import * as TestOverrides from '../../test-related-lib/TestOverrides'; -import {CUSTOM_CONFIG, ENGINE, LANGUAGE, PMD6_VERSION} from '../../../src/Constants'; +import {CUSTOM_CONFIG, ENGINE, LANGUAGE, PMD6_VERSION, PMD7_LIB, PMD7_VERSION} from '../../../src/Constants'; import * as DataGenerator from '../eslint/EslintTestDataGenerator'; import {BundleName, getMessage} from "../../../src/MessageCatalog"; import {Config} from "../../../src/lib/util/Config"; import {CustomRulePathManager} from "../../../src/lib/CustomRulePathManager"; import {after} from "mocha"; +import {Pmd7CommandInfo, PmdCommandInfo} from "../../../src/lib/pmd/PmdCommandInfo"; +import {Controller} from "../../../src/Controller"; TestOverrides.initializeTestSetup(); @@ -450,4 +452,24 @@ describe('_PmdRuleMapper', () => { Sinon.assert.calledWith(uxSpy, EVENTS.WARNING_ALWAYS, `Custom rule file path [${missingJar}] for language [${LANGUAGE.JAVA}] was not found.`); }); }); + + describe('Using PMD7', async () => { + const originalPmdCommandInfo: PmdCommandInfo = Controller.getActivePmdCommandInfo() + before(() => { + Controller.setActivePmdCommandInfo(new Pmd7CommandInfo()); + }); + after(() => { + Controller.setActivePmdCommandInfo(originalPmdCommandInfo); + }) + + it('PMD7 lib jar files are found correctly', async () => { + const mapper = await _PmdRuleMapper.create({}); + const ruleMap = await mapper.createStandardRuleMap(); + expect(ruleMap.size).to.greaterThan(0); + ruleMap.forEach((jars: Set, language: string) => { + expect(jars.size).to.equal(1); + expect(jars).to.contain(path.join(PMD7_LIB, `pmd-${language}-${PMD7_VERSION}.jar`)); + }) + }) + }); });