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: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/code-analyzer-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/code-analyzer-core",
"description": "Core Package for the Salesforce Code Analyzer",
"version": "0.32.0",
"version": "0.33.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand Down Expand Up @@ -69,4 +69,4 @@
"!src/index.ts"
]
}
}
}
5 changes: 4 additions & 1 deletion packages/code-analyzer-core/src/output-format.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Clock, RealClock } from '@salesforce/code-analyzer-engine-api/utils';
import { CsvRunResultsFormatter } from "./output-formats/results/csv-run-results-format";
import { HtmlRunResultsFormatter } from "./output-formats/results/html-run-results-format";
import { HtmlRunResultsFormatter} from "./output-formats/results/html-run-results-format";
import { JsonRunResultsFormatter } from "./output-formats/results/json-run-results-format";
import { SarifRunResultsFormatter } from "./output-formats/results/sarif-run-results-format";
import { XmlRunResultsFormatter } from "./output-formats/results/xml-run-results-format";
import { JsonRulesFormatter } from "./output-formats/rules/json-rules-format";
import { CsvRulesFormatter } from "./output-formats/rules/csv-rules-format";
import { RunResults } from "./results";
import { RuleSelection } from "./rules";

Expand Down Expand Up @@ -74,6 +75,8 @@ export abstract class RuleSelectionFormatter {
switch (format) {
case OutputFormat.JSON:
return new JsonRulesFormatter();
case OutputFormat.CSV:
return new CsvRulesFormatter();
default:
throw new Error(`Unsupported output format: ${format}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { stringify as stringifyToCsv } from "csv-stringify/sync";
import { Options as CsvOptions } from "csv-stringify";
import { RuleSelectionFormatter } from "../../output-format";
import { Rule, RuleSelection } from "../../rules";

export class CsvRulesFormatter implements RuleSelectionFormatter {
format(ruleSelection:RuleSelection): string {
const selectedRules: Rule[] = ruleSelection.getEngineNames().flatMap(name => ruleSelection.getRulesFor(name));
const csvRows: CsvRow[] = selectedRules.map(toCsvRow);
const options: CsvOptions = {
header: true,
quoted_string: true,
columns: ["name", "severity", "engine", "tags", "resources", "description"],
cast: {
object: value => {
/* istanbul ignore else */
if (Array.isArray(value)) {
return { value: value.join(','), quoted: true};
}
/* istanbul ignore next */
throw new Error(`Unsupported value to cast: ${value}.`);
}
}
}
return stringifyToCsv(csvRows, options);
}
}

type CsvRow = {
name: string
engine: string
description: string
severity: number
tags: string[]
resources?: string[]
}

function toCsvRow(rule: Rule): CsvRow {
return {
name: rule.getName(),
engine: rule.getEngineName(),
description: rule.getDescription(),
severity: rule.getSeverityLevel(),
tags: rule.getTags(),
resources: rule.getResourceUrls()
};
}
37 changes: 35 additions & 2 deletions packages/code-analyzer-core/test/output-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import * as fs from "fs";
import path from "node:path";
import { CodeAnalyzer, CodeAnalyzerConfig, OutputFormat } from "../src";
import { RunResults, RunResultsImpl } from "../src/results";
import { RuleSelection, RuleSelectionImpl } from "../src/rules";
import { RuleImpl, RuleSelection, RuleSelectionImpl } from "../src/rules";
import * as stubs from "./stubs";
import { FixedClock } from "@salesforce/code-analyzer-engine-api/utils";
import { changeWorkingDirectoryToPackageRoot } from "./test-helpers";
import {SeverityLevel} from "@salesforce/code-analyzer-engine-api";

changeWorkingDirectoryToPackageRoot();

Expand Down Expand Up @@ -197,6 +198,38 @@ describe("RuleSelectionFormatter Tests", () => {
});
});

describe("Tests for the CSV output format", () => {
it("When no rules are selected, we create a CSV with headers but no rows", () => {
const emptyRules: RuleSelection = new RuleSelectionImpl();
const formattedText: string = emptyRules.toFormattedOutput(OutputFormat.CSV);
const expectedText: string = getContentsOfExpectedOutputFile('zeroRules.goldfile.csv', true, true);
expect(formattedText).toEqual(expectedText);
});

it("When multiple rules are selected, we create a CSV with populated rows", () => {
const complicatedRuleSelection: RuleSelectionImpl = new RuleSelectionImpl();
const rule1: RuleImpl = new RuleImpl('stubEngine1', {
name: 'stub1RuleA',
severityLevel: SeverityLevel.Moderate,
tags: ['Recommended', 'CodeStyle'],
description: 'A rule description that contains\na new line character, as well as `ticks`, "double quotes", \'single quotes\,\n<brackets>, and even {curly braces}!',
resourceUrls: ['https://example.com/stub1RuleA', 'https://example.com/stub1RuleA_2']
});
const rule2: RuleImpl = new RuleImpl('stubEngine1', {
name: 'stub1RuleB',
severityLevel: SeverityLevel.Low,
tags: ['Recommended', 'Performance'],
description: 'A simple description this time',
resourceUrls: []
});
complicatedRuleSelection.addRule(rule1);
complicatedRuleSelection.addRule(rule2);
Comment on lines +210 to +226

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rather than modify the actual hardcoded description of any of the stub rules (which would have likely impacted other tests), I decided it was just easier to instantiate a rule selection from scratch with some made up rules.
It's my opinion that this is acceptable, but I'm prepared for disagreement.

const formattedText: string = complicatedRuleSelection.toFormattedOutput(OutputFormat.CSV);
const expectedText: string = getContentsOfExpectedOutputFile('multipleRules.goldfile.csv', true, true);
expect(formattedText).toEqual(expectedText);
});
});

describe("Other misc output formatting tests", () => {
it("When an output format is not supported, then we error", () => {
const rules: RuleSelection = new RuleSelectionImpl();
Expand Down Expand Up @@ -244,5 +277,5 @@ async function createRulesWithEmptyTags(): Promise<RuleSelection> {
const codeAnalyzer: CodeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults());
codeAnalyzer._setClock(new FixedClock(fixedTime));
await codeAnalyzer.addEnginePlugin(new stubs.EmptyTagEnginePlugin());
return await codeAnalyzer.selectRules(['all'])
return codeAnalyzer.selectRules(['all'])
}
1 change: 0 additions & 1 deletion packages/code-analyzer-core/test/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,6 @@ class EmptyTagEngine extends engApi.Engine {
}
}


/**
* FutureEnginePlugin - A plugin to help with testing forward compatibility
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"name","severity","engine","tags","resources","description"
"stub1RuleA",3,"stubEngine1","Recommended,CodeStyle","https://example.com/stub1RuleA,https://example.com/stub1RuleA_2","A rule description that contains
a new line character, as well as `ticks`, ""double quotes"", 'single quotes,
<brackets>, and even {curly braces}!"
"stub1RuleB",4,"stubEngine1","Recommended,Performance",,"A simple description this time"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"name","severity","engine","tags","resources","description"
Loading