Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -133,6 +134,7 @@ export enum Severity {

// Here, current dir __dirname = <base_dir>/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 = <base_dir>/sfdx-scanner/src
export const APPEXCHANGE_PMD_LIB = path.join(__dirname, '..', 'pmd-appexchange', 'lib');
14 changes: 13 additions & 1 deletion src/lib/Display.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -27,6 +28,11 @@ export interface Display {
*/
displayTable<R extends Ux.Table.Data>(data: R[], columns: Ux.Table.Columns<R>): void;

/**
* Output object to stdout only if the "--json" flag is not present.
*/
displayStyledObject(obj: AnyJson): void;

/**
* Display a message as a warning.
*/
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<boolean>;

// 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<R extends Ux.Table.Data>(data: R[], columns: Ux.Table.Columns<R>, options?: Ux.Table.Options): void;
}
24 changes: 15 additions & 9 deletions src/lib/actions/RuleDescribeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -34,8 +33,6 @@ export class RuleDescribeAction implements Action {
}

public async run(inputs: Inputs): Promise<AnyJson> {
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
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Comment on lines -91 to +87
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling this TODO right now because the testability is impossible without having this data tracked on our own Display object.

I confirmed that this did not break rule describe or rule describe --json in any way. Still outputting the same information and all of our other tests around this still pass.

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
})
}
}
8 changes: 4 additions & 4 deletions src/lib/cpd/CpdEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Copy link
Contributor Author

@stephen-carter-at-sf stephen-carter-at-sf Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that "as number" doesn't actually change the type here from string to number. So using "Number(...)" instead so that I can actually test it as a number.

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]),
Expand Down
32 changes: 30 additions & 2 deletions src/lib/pmd/PmdCommandInfo.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a lot of code here at all. But I had to add quite a bit of testing (because of the poor health of our test code). All this new code is covered 100%.

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'];
}
}
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type Rule = {
// be OR'd together in this property.
defaultConfig?: ESRuleConfigValue;
url?: string;
message?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In RuleDescribeAction, we referenced the 'message' field dynamically... but now i'm referencing it statically. This wasn't possible because we were missing the message field from our type here. So I fixed this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand what a "message" would be in a Rule object? "description" would be the description of the rule and the Violation type should have information that the engine returns, so I'm not sure what a "message" would be here.

Copy link
Contributor Author

@stephen-carter-at-sf stephen-carter-at-sf Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example (even before my changes):
sf scanner rule describe -n ApexCRUDViolation
Would give a bunch of information including:
message: Validate CRUD permission before SOQL/DML operation or enforce user mode

}

export type TelemetryData = {
Expand Down
2 changes: 1 addition & 1 deletion test/commands/scanner/rule/describe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
11 changes: 11 additions & 0 deletions test/lib/FakeDisplay.ts
Original file line number Diff line number Diff line change
@@ -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<Ux.Table.Data>;
private lastTableData: Ux.Table.Data[];
private lastStyledObject: AnyJson;

public getOutputArray(): string[] {
return this.outputs;
Expand All @@ -27,6 +29,10 @@ export class FakeDisplay implements Display {
return this.lastTableData;
}

public getLastStyledObject(): AnyJson {
return this.lastStyledObject;
}


displayConfirmationPrompt(msg: string): Promise<boolean> {
this.outputs.push(msg);
Expand Down Expand Up @@ -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)
Expand Down
69 changes: 69 additions & 0 deletions test/lib/actions/RuleDescribeAction.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
72 changes: 72 additions & 0 deletions test/lib/actions/RuleListAction.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
});
Loading