Skip to content

Commit d4f6406

Browse files
authored
NEW @W-17977608@ CSV output format for rules (#326)
1 parent f21a7c8 commit d4f6406

File tree

8 files changed

+95
-7
lines changed

8 files changed

+95
-7
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/code-analyzer-core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-core",
33
"description": "Core Package for the Salesforce Code Analyzer",
4-
"version": "0.32.0",
4+
"version": "0.33.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
@@ -69,4 +69,4 @@
6969
"!src/index.ts"
7070
]
7171
}
72-
}
72+
}

packages/code-analyzer-core/src/output-format.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Clock, RealClock } from '@salesforce/code-analyzer-engine-api/utils';
22
import { CsvRunResultsFormatter } from "./output-formats/results/csv-run-results-format";
3-
import { HtmlRunResultsFormatter } from "./output-formats/results/html-run-results-format";
3+
import { HtmlRunResultsFormatter} from "./output-formats/results/html-run-results-format";
44
import { JsonRunResultsFormatter } from "./output-formats/results/json-run-results-format";
55
import { SarifRunResultsFormatter } from "./output-formats/results/sarif-run-results-format";
66
import { XmlRunResultsFormatter } from "./output-formats/results/xml-run-results-format";
77
import { JsonRulesFormatter } from "./output-formats/rules/json-rules-format";
8+
import { CsvRulesFormatter } from "./output-formats/rules/csv-rules-format";
89
import { RunResults } from "./results";
910
import { RuleSelection } from "./rules";
1011

@@ -74,6 +75,8 @@ export abstract class RuleSelectionFormatter {
7475
switch (format) {
7576
case OutputFormat.JSON:
7677
return new JsonRulesFormatter();
78+
case OutputFormat.CSV:
79+
return new CsvRulesFormatter();
7780
default:
7881
throw new Error(`Unsupported output format: ${format}`);
7982
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { stringify as stringifyToCsv } from "csv-stringify/sync";
2+
import { Options as CsvOptions } from "csv-stringify";
3+
import { RuleSelectionFormatter } from "../../output-format";
4+
import { Rule, RuleSelection } from "../../rules";
5+
6+
export class CsvRulesFormatter implements RuleSelectionFormatter {
7+
format(ruleSelection:RuleSelection): string {
8+
const selectedRules: Rule[] = ruleSelection.getEngineNames().flatMap(name => ruleSelection.getRulesFor(name));
9+
const csvRows: CsvRow[] = selectedRules.map(toCsvRow);
10+
const options: CsvOptions = {
11+
header: true,
12+
quoted_string: true,
13+
columns: ["name", "severity", "engine", "tags", "resources", "description"],
14+
cast: {
15+
object: value => {
16+
/* istanbul ignore else */
17+
if (Array.isArray(value)) {
18+
return { value: value.join(','), quoted: true};
19+
}
20+
/* istanbul ignore next */
21+
throw new Error(`Unsupported value to cast: ${value}.`);
22+
}
23+
}
24+
}
25+
return stringifyToCsv(csvRows, options);
26+
}
27+
}
28+
29+
type CsvRow = {
30+
name: string
31+
engine: string
32+
description: string
33+
severity: number
34+
tags: string[]
35+
resources?: string[]
36+
}
37+
38+
function toCsvRow(rule: Rule): CsvRow {
39+
return {
40+
name: rule.getName(),
41+
engine: rule.getEngineName(),
42+
description: rule.getDescription(),
43+
severity: rule.getSeverityLevel(),
44+
tags: rule.getTags(),
45+
resources: rule.getResourceUrls()
46+
};
47+
}

packages/code-analyzer-core/test/output-format.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import * as fs from "fs";
22
import path from "node:path";
33
import { CodeAnalyzer, CodeAnalyzerConfig, OutputFormat } from "../src";
44
import { RunResults, RunResultsImpl } from "../src/results";
5-
import { RuleSelection, RuleSelectionImpl } from "../src/rules";
5+
import { RuleImpl, RuleSelection, RuleSelectionImpl } from "../src/rules";
66
import * as stubs from "./stubs";
77
import { FixedClock } from "@salesforce/code-analyzer-engine-api/utils";
88
import { changeWorkingDirectoryToPackageRoot } from "./test-helpers";
9+
import {SeverityLevel} from "@salesforce/code-analyzer-engine-api";
910

1011
changeWorkingDirectoryToPackageRoot();
1112

@@ -197,6 +198,38 @@ describe("RuleSelectionFormatter Tests", () => {
197198
});
198199
});
199200

201+
describe("Tests for the CSV output format", () => {
202+
it("When no rules are selected, we create a CSV with headers but no rows", () => {
203+
const emptyRules: RuleSelection = new RuleSelectionImpl();
204+
const formattedText: string = emptyRules.toFormattedOutput(OutputFormat.CSV);
205+
const expectedText: string = getContentsOfExpectedOutputFile('zeroRules.goldfile.csv', true, true);
206+
expect(formattedText).toEqual(expectedText);
207+
});
208+
209+
it("When multiple rules are selected, we create a CSV with populated rows", () => {
210+
const complicatedRuleSelection: RuleSelectionImpl = new RuleSelectionImpl();
211+
const rule1: RuleImpl = new RuleImpl('stubEngine1', {
212+
name: 'stub1RuleA',
213+
severityLevel: SeverityLevel.Moderate,
214+
tags: ['Recommended', 'CodeStyle'],
215+
description: 'A rule description that contains\na new line character, as well as `ticks`, "double quotes", \'single quotes\,\n<brackets>, and even {curly braces}!',
216+
resourceUrls: ['https://example.com/stub1RuleA', 'https://example.com/stub1RuleA_2']
217+
});
218+
const rule2: RuleImpl = new RuleImpl('stubEngine1', {
219+
name: 'stub1RuleB',
220+
severityLevel: SeverityLevel.Low,
221+
tags: ['Recommended', 'Performance'],
222+
description: 'A simple description this time',
223+
resourceUrls: []
224+
});
225+
complicatedRuleSelection.addRule(rule1);
226+
complicatedRuleSelection.addRule(rule2);
227+
const formattedText: string = complicatedRuleSelection.toFormattedOutput(OutputFormat.CSV);
228+
const expectedText: string = getContentsOfExpectedOutputFile('multipleRules.goldfile.csv', true, true);
229+
expect(formattedText).toEqual(expectedText);
230+
});
231+
});
232+
200233
describe("Other misc output formatting tests", () => {
201234
it("When an output format is not supported, then we error", () => {
202235
const rules: RuleSelection = new RuleSelectionImpl();
@@ -244,5 +277,5 @@ async function createRulesWithEmptyTags(): Promise<RuleSelection> {
244277
const codeAnalyzer: CodeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults());
245278
codeAnalyzer._setClock(new FixedClock(fixedTime));
246279
await codeAnalyzer.addEnginePlugin(new stubs.EmptyTagEnginePlugin());
247-
return await codeAnalyzer.selectRules(['all'])
280+
return codeAnalyzer.selectRules(['all'])
248281
}

packages/code-analyzer-core/test/stubs.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,6 @@ class EmptyTagEngine extends engApi.Engine {
474474
}
475475
}
476476

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

0 commit comments

Comments
 (0)