Skip to content

Commit

Permalink
NEW: @W-15806472@: Add in CSV, JSON, and XML result formats (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephen-carter-at-sf authored Jun 5, 2024
1 parent 1bd64b0 commit 8d9aab2
Show file tree
Hide file tree
Showing 26 changed files with 2,048 additions and 1,050 deletions.
1,937 changes: 1,041 additions & 896 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 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.1.2",
"version": "0.2.0",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause license",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand All @@ -13,13 +13,15 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@salesforce/code-analyzer-engine-api": "0.1.2",
"@salesforce/code-analyzer-engine-api": "0.2.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"js-yaml": "^4.1.0"
"csv-stringify": "^6.5.0",
"js-yaml": "^4.1.0",
"xmlbuilder": "^15.1.1"
},
"devDependencies": {
"@eslint/js": "^9.2.0",
"@eslint/js": "^8.57.0",
"@types/jest": "^29.0.0",
"eslint": "^8.57.0",
"jest": "^29.0.0",
Expand Down
33 changes: 25 additions & 8 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {RuleImpl, RuleSelection, RuleSelectionImpl} from "./rules"
import {EngineRunResults, EngineRunResultsImpl, RunResults, RunResultsImpl} from "./results"
import {
EngineRunResults,
EngineRunResultsImpl,
RunResults,
RunResultsImpl,
UnexpectedErrorEngineRunResults
} from "./results"
import {EngineLogEvent, EngineProgressEvent, EngineResultsEvent, Event, EventType, LogLevel} from "./events"
import {getMessage} from "./messages";
import * as engApi from "@salesforce/code-analyzer-engine-api"
Expand Down Expand Up @@ -47,7 +53,7 @@ export class CodeAnalyzer {
}

public selectRules(...selectors: string[]): RuleSelection {
selectors = selectors.length > 0 ? selectors : ['default'];
selectors = selectors.length > 0 ? selectors : ['Recommended'];

const ruleSelection: RuleSelectionImpl = new RuleSelectionImpl();
for (const rule of this.getAllRules()) {
Expand All @@ -68,12 +74,7 @@ export class CodeAnalyzer {
type: EventType.EngineProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0
});

const rulesToRun: string[] = ruleSelection.getRulesFor(engineName).map(r => r.getName());
this.emitLogEvent(LogLevel.Debug, getMessage('RunningEngineWithRules', engineName, JSON.stringify(rulesToRun)));
const engine: engApi.Engine = this.getEngine(engineName);
const apiEngineRunResults: engApi.EngineRunResults = engine.runRules(rulesToRun, engineRunOptions);
validateEngineRunResults(engineName, apiEngineRunResults, ruleSelection);
const engineRunResults: EngineRunResults = new EngineRunResultsImpl(engineName, apiEngineRunResults, ruleSelection);
const engineRunResults: EngineRunResults = this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions);
runResults.addEngineRunResults(engineRunResults);

this.emitEvent<EngineProgressEvent>({
Expand All @@ -91,6 +92,22 @@ export class CodeAnalyzer {
this.eventEmitter.on(eventType, callback);
}

private runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions): EngineRunResults {
const rulesToRun: string[] = ruleSelection.getRulesFor(engineName).map(r => r.getName());
this.emitLogEvent(LogLevel.Debug, getMessage('RunningEngineWithRules', engineName, JSON.stringify(rulesToRun)));
const engine: engApi.Engine = this.getEngine(engineName);

let apiEngineRunResults: engApi.EngineRunResults;
try {
apiEngineRunResults = engine.runRules(rulesToRun, engineRunOptions);
} catch (error) {
return new UnexpectedErrorEngineRunResults(engineName, error as Error);
}

validateEngineRunResults(engineName, apiEngineRunResults, ruleSelection);
return new EngineRunResultsImpl(engineName, apiEngineRunResults, ruleSelection);
}

private emitEvent<T extends Event>(event: T): void {
this.eventEmitter.emit(event.type, event);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/code-analyzer-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ export {
LogLevel
} from "./events"

export {
OutputFormat,
OutputFormatter
} from "./output-format"

export {
CodeLocation,
EngineRunResults,
OutputFormatter,
RunResults,
Violation
} from "./results"
Expand Down
10 changes: 8 additions & 2 deletions packages/code-analyzer-core/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ const messageCatalog : { [key: string]: string } = {
EngineValidationFailed:
'Failed to add engine with name "%s" because it failed validation:\n%s',

UnexpectedEngineErrorRuleDescription:
'This rule reports a violation when an unexpected error occurs from engine "%s".',

UnexpectedEngineErrorViolationMessage:
'The engine with name "%s" threw an unexpected error: %s',

PluginErrorFromGetAvailableEngineNames:
`Failed to add engine plugin since the plugin's getAvailableNames method through an error:\n%s`,
`Failed to add engine plugin since the plugin's getAvailableNames method threw an error:\n%s`,

PluginErrorFromCreateEngine:
`Failed to create engine with name "%s" since the plugin's createEngine method through an error:\n%s`,
`Failed to create engine with name "%s" since the plugin's createEngine method threw an error:\n%s`,

EngineAdded:
'Engine with name "%s" was added to Code Analyzer.',
Expand Down
221 changes: 221 additions & 0 deletions packages/code-analyzer-core/src/output-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import {CodeLocation, RunResults, Violation} from "./results";
import {Rule, RuleType, SeverityLevel} from "./rules";
import {stringify as stringifyToCsv} from "csv-stringify/sync";
import {Options as CsvOptions} from "csv-stringify";
import * as xmlbuilder from "xmlbuilder";

export enum OutputFormat {
CSV = "CSV",
JSON = "JSON",
XML = "XML"
}

export abstract class OutputFormatter {
abstract format(results: RunResults): string

static forFormat(format: OutputFormat) {
switch (format) {
case OutputFormat.CSV:
return new CsvOutputFormatter();
case OutputFormat.JSON:
return new JsonOutputFormatter();
case OutputFormat.XML:
return new XmlOutputFormatter();
default:
throw new Error(`Unsupported output format: ${format}`);
}
}
}

type ResultsOutput = {
runDir: string
violationCounts: {
total: number
sev1: number
sev2: number
sev3: number
sev4: number
sev5: number
}
violations: ViolationOutput[]
}

type ViolationOutput = {
id: number
rule: string
engine: string
severity: number
type: string
tags: string[]
file?: string
line?: number
column?: number
endLine?: number
endColumn?: number
pathLocations?: string[]
message: string
resources?: string[]
}

class CsvOutputFormatter implements OutputFormatter {
format(results: RunResults): string {
const violationOutputs: ViolationOutput[] = toViolationOutputs(results.getViolations(), results.getRunDirectory());
const options: CsvOptions = {
header: true,
quoted_string: true,
columns: ['id', 'rule', 'engine', 'severity', 'type', 'tags', 'file', 'line', 'column',
'endLine', 'endColumn', 'pathLocations', 'message', 'resources'],
cast: {
object: value => {
if (Array.isArray(value)) {
return { value: value.join(','), quoted: true };
}
/* istanbul ignore next */
throw new Error(`Unsupported value to cast: ${value}.`)
}
}
};
return stringifyToCsv(violationOutputs, options);
}
}

class JsonOutputFormatter implements OutputFormatter {
format(results: RunResults): string {
const resultsOutput: ResultsOutput = toResultsOutput(results);
return JSON.stringify(resultsOutput, undefined, 2);
}
}

class XmlOutputFormatter implements OutputFormatter {
format(results: RunResults): string {
const resultsOutput: ResultsOutput = toResultsOutput(results);

const resultsNode: xmlbuilder.XMLElement = xmlbuilder.create('results', {version: '1.0', encoding: 'UTF-8'});
resultsNode.node('runDir').text(resultsOutput.runDir);
const violationCountsNode: xmlbuilder.XMLElement = resultsNode.node('violationCounts');
violationCountsNode.node('total').text(`${resultsOutput.violationCounts.total}`);
violationCountsNode.node('sev1').text(`${resultsOutput.violationCounts.sev1}`);
violationCountsNode.node('sev2').text(`${resultsOutput.violationCounts.sev2}`);
violationCountsNode.node('sev3').text(`${resultsOutput.violationCounts.sev3}`);
violationCountsNode.node('sev4').text(`${resultsOutput.violationCounts.sev4}`);
violationCountsNode.node('sev5').text(`${resultsOutput.violationCounts.sev5}`);

const violationsNode: xmlbuilder.XMLElement = resultsNode.node('violations');
for (const violationOutput of resultsOutput.violations) {
const violationNode: xmlbuilder.XMLElement = violationsNode.node('violation');
violationNode.attribute('id', violationOutput.id);
violationNode.node('rule').text(violationOutput.rule);
violationNode.node('engine').text(violationOutput.engine);
violationNode.node('severity').text(`${violationOutput.severity}`);
violationNode.node('type').text(violationOutput.type);
const tagsNode: xmlbuilder.XMLElement = violationNode.node('tags');
for (const tag of violationOutput.tags) {
tagsNode.node('tag').text(tag);
}
if (violationOutput.file) {
violationNode.node('file').text(violationOutput.file);
}
if (violationOutput.line) {
violationNode.node('line').text(`${violationOutput.line}`);
}
if (violationOutput.column) {
violationNode.node('column').text(`${violationOutput.column}`);
}
if (violationOutput.endLine) {
violationNode.node('endLine').text(`${violationOutput.endLine}`);
}
if (violationOutput.endColumn) {
violationNode.node('endColumn').text(`${violationOutput.endColumn}`);
}
if (violationOutput.pathLocations) {
const pathLocationsNode: xmlbuilder.XMLElement = violationNode.node('pathLocations');
for (const pathLocation of violationOutput.pathLocations) {
pathLocationsNode.node('pathLocation', pathLocation);
}
}
violationNode.node('message').text(violationOutput.message);
if (violationOutput.resources) {
const resourcesNode: xmlbuilder.XMLElement = violationNode.node('resources');
for (const resource of violationOutput.resources) {
resourcesNode.node('resource').text(resource);
}
}
}

return violationsNode.end({ pretty: true, allowEmpty: true });
}
}

function toResultsOutput(results: RunResults) {
const resultsOutput: ResultsOutput = {
runDir: results.getRunDirectory(),
violationCounts: {
total: results.getViolationCount(),
sev1: results.getViolationCountOfSeverity(SeverityLevel.Critical),
sev2: results.getViolationCountOfSeverity(SeverityLevel.High),
sev3: results.getViolationCountOfSeverity(SeverityLevel.Moderate),
sev4: results.getViolationCountOfSeverity(SeverityLevel.Low),
sev5: results.getViolationCountOfSeverity(SeverityLevel.Info),
},
violations: toViolationOutputs(results.getViolations(), results.getRunDirectory())
};
return resultsOutput;
}

function toViolationOutputs(violations: Violation[], runDir: string): ViolationOutput[] {
const violationOutputs: ViolationOutput[] = [];
for (let i = 0; i < violations.length; i++) {
const violation: Violation = violations[i];
const row: ViolationOutput = createViolationOutput(i+1, violation, runDir);
violationOutputs.push(row)
}
return violationOutputs;
}

function createViolationOutput(id: number, violation: Violation, runDir: string): ViolationOutput {
const rule: Rule = violation.getRule();
const codeLocations: CodeLocation[] = violation.getCodeLocations();
const primaryLocation: CodeLocation = codeLocations[violation.getPrimaryLocationIndex()];

return {
id: id,
rule: rule.getName(),
engine: rule.getEngineName(),
severity: rule.getSeverityLevel(),
type: rule.getType(),
tags: rule.getTags(),
file: primaryLocation.getFile() ? makeRelativeIfPossible(primaryLocation.getFile() as string, runDir) : undefined,
line: primaryLocation.getStartLine(),
column: primaryLocation.getStartColumn(),
endLine: primaryLocation.getEndLine(),
endColumn: primaryLocation.getEndColumn(),
pathLocations: [RuleType.DataFlow, RuleType.Flow].includes(rule.getType()) ? createPathLocations(codeLocations, runDir) : undefined,
message: violation.getMessage(),
resources: violation.getRule().getResourceUrls()
};
}

function createPathLocations(codeLocations: CodeLocation[], runDir: string): string[] {
return codeLocations.map(l => createLocationString(l, runDir)).filter(s => s.length > 0);
}

function createLocationString(codeLocation: CodeLocation, runDir: string): string {
let locationString: string = '';
if (codeLocation.getFile()) {
locationString += makeRelativeIfPossible(codeLocation.getFile() as string, runDir);
if (codeLocation.getStartLine()) {
locationString += ':' + codeLocation.getStartLine();
if (codeLocation.getStartColumn()) {
locationString += ':' + codeLocation.getStartColumn();
}
}
}
return locationString;
}

function makeRelativeIfPossible(file: string, rootDir: string): string {
if (file.startsWith(rootDir)) {
file = file.substring(rootDir.length);
}
return file;
}
Loading

0 comments on commit 8d9aab2

Please sign in to comment.