diff --git a/package-lock.json b/package-lock.json index 880d8f66..c4a23148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11168,7 +11168,7 @@ }, "packages/code-analyzer-sfge-engine": { "name": "@salesforce/code-analyzer-sfge-engine", - "version": "0.10.0", + "version": "0.11.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@salesforce/code-analyzer-engine-api": "0.27.0", diff --git a/packages/code-analyzer-core/package.json b/packages/code-analyzer-core/package.json index 25c8258d..0bba533c 100644 --- a/packages/code-analyzer-core/package.json +++ b/packages/code-analyzer-core/package.json @@ -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", @@ -69,4 +69,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-core/src/output-format.ts b/packages/code-analyzer-core/src/output-format.ts index fea12b6f..2cf24685 100644 --- a/packages/code-analyzer-core/src/output-format.ts +++ b/packages/code-analyzer-core/src/output-format.ts @@ -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"; @@ -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}`); } diff --git a/packages/code-analyzer-core/src/output-formats/rules/csv-rules-format.ts b/packages/code-analyzer-core/src/output-formats/rules/csv-rules-format.ts new file mode 100644 index 00000000..660c1fef --- /dev/null +++ b/packages/code-analyzer-core/src/output-formats/rules/csv-rules-format.ts @@ -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() + }; +} diff --git a/packages/code-analyzer-core/test/output-format.test.ts b/packages/code-analyzer-core/test/output-format.test.ts index 5129b56f..f579ca95 100644 --- a/packages/code-analyzer-core/test/output-format.test.ts +++ b/packages/code-analyzer-core/test/output-format.test.ts @@ -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(); @@ -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, 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); + 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(); @@ -244,5 +277,5 @@ async function createRulesWithEmptyTags(): Promise { 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']) } diff --git a/packages/code-analyzer-core/test/stubs.ts b/packages/code-analyzer-core/test/stubs.ts index 0cb92c59..1ffbc75e 100644 --- a/packages/code-analyzer-core/test/stubs.ts +++ b/packages/code-analyzer-core/test/stubs.ts @@ -474,7 +474,6 @@ class EmptyTagEngine extends engApi.Engine { } } - /** * FutureEnginePlugin - A plugin to help with testing forward compatibility */ diff --git a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleRules.goldfile.csv b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleRules.goldfile.csv new file mode 100644 index 00000000..34fca9eb --- /dev/null +++ b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/multipleRules.goldfile.csv @@ -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, +, and even {curly braces}!" +"stub1RuleB",4,"stubEngine1","Recommended,Performance",,"A simple description this time" diff --git a/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroRules.goldfile.csv b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroRules.goldfile.csv new file mode 100644 index 00000000..7079af71 --- /dev/null +++ b/packages/code-analyzer-core/test/test-data/expectedOutputFiles/zeroRules.goldfile.csv @@ -0,0 +1 @@ +"name","severity","engine","tags","resources","description"