Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 6 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/code-analyzer-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@salesforce/code-analyzer-engine-api": "0.12.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"@types/sarif": "^2.1.7",
"csv-stringify": "^6.5.0",
"js-yaml": "^4.1.0",
"xmlbuilder": "^15.1.1"
Expand Down
135 changes: 134 additions & 1 deletion packages/code-analyzer-core/src/output-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import * as xmlbuilder from "xmlbuilder";
import * as fs from 'fs';
import path from "node:path";
import {Clock, RealClock} from "./utils";
import * as sarif from "sarif";

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

export abstract class OutputFormatter {
Expand All @@ -27,6 +29,8 @@ export abstract class OutputFormatter {
return new XmlOutputFormatter();
case OutputFormat.HTML:
return new HtmlOutputFormatter(clock);
case OutputFormat.SARIF:
return new SarifOutputFormatter();
default:
throw new Error(`Unsupported output format: ${format}`);
}
Expand Down Expand Up @@ -231,6 +235,135 @@ class XmlOutputFormatter implements OutputFormatter {
}
}

class SarifOutputFormatter implements OutputFormatter {
format(results: RunResults): string {
const resultsOutput: ResultsOutput = toResultsOutput(results);
const resultsByEngine: Map<string, ViolationOutput[]> = new Map<string, ViolationOutput[]>();

for (const engine of results.getEngineNames()) {
resultsByEngine.set(engine, []);
}

for (const violation of resultsOutput.violations) {
resultsByEngine.get(violation.engine)?.push(violation);
}

const sarifRuns : sarif.Run[] = [];
for (const [engine, violations] of resultsByEngine.entries()) {
const ruleMap = new Map<string, number>();
// Convert violations to SARIF results
const rules = this.populateRuleMap(violations, ruleMap);
const sarifResults: sarif.Result[] = violations.map(violation => {

const location: sarif.Location = {
physicalLocation: {
artifactLocation: {
uri: violation.file,
},
region: {
startLine: violation.line,
startColumn: violation.column,
endLine: violation.endLine,
endColumn: violation.endColumn
} as sarif.Region
}
};

const relatedLocations:sarif.Location[] = [];
let locIndex:number = 0;
violation.locations?.forEach(violationLocation => {
if (locIndex != violation.primaryLocationIndex) {
const relatedLocation: sarif.Location = {
physicalLocation: {
artifactLocation: {
uri: violationLocation.getFile(),
},
region: {
startLine: violationLocation.getLine(),
startColumn: violationLocation.getColumn(),
endLine: violationLocation.getEndLine(),
endColumn: violationLocation.getEndColumn(),
} as sarif.Region,
},
};
relatedLocations.push(relatedLocation);
locIndex++;
}
});
const result: sarif.Result = {
ruleId: violation.rule,
ruleIndex: ruleMap.get(violation.rule),
message: { text: violation.message },
locations: [location],
relatedLocations: relatedLocations,
level: this.getLevel(violation.severity),
};

return result;
});

// Define SARIF tool with ruleset information
const run: sarif.Run = {
tool: {
driver: {
name: engine,
informationUri: "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/version-5.html",
rules: rules,
}
},
results: sarifResults,
invocations: [
{
executionSuccessful: true,
workingDirectory: {
uri: results.getRunDirectory(),
},
},
],
};
sarifRuns.push(run);
}

// Construct SARIF log
const sarif: sarif.Log = {
version: "2.1.0",
$schema: 'http://json.schemastore.org/sarif-2.1.0',
runs: sarifRuns,
};

// Return formatted SARIF JSON string
return JSON.stringify(sarif, null, 2);
}

private getLevel(ruleViolation: number): sarif.Notification.level {
return ruleViolation < 3 ? 'error' : 'warning';
}

private populateRuleMap(violations: ViolationOutput[], ruleMap: Map<string, number>): sarif.ReportingDescriptor[] {
const rules: sarif.ReportingDescriptor[] = [];
for (const v of violations) {
if (!ruleMap.has(v.rule)) {
ruleMap.set(v.rule, ruleMap.size);
const rule = {
id: v.rule,
properties: {
category: v.tags,
severity: v.severity
},
helpUri: ''
};
if (v.resources) {
rule['helpUri'] = v.resources[0];
}
rules.push(rule);
}
}

return rules;
}
}


const HTML_TEMPLATE_VERSION: string = '0.0.1';
const HTML_TEMPLATE_FILE: string = path.resolve(__dirname, '..', 'output-templates', `html-template-${HTML_TEMPLATE_VERSION}.txt`);
class HtmlOutputFormatter implements OutputFormatter {
Expand Down
22 changes: 22 additions & 0 deletions packages/code-analyzer-core/test/output-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ describe("Tests for the XML output format", () => {
});
});

describe("Tests for the SARIF output format", () => {
it("When an empty result is provided, we create a sarif text with summary having zeros", () => {
const results: RunResults = new RunResultsImpl();
const formattedText: string = results.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('zeroViolations.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});

it("When results contain multiple violations , we create sarif text correctly", () => {
const formattedText: string = runResults.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('multipleViolations.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});

it("When results contain violation of type UnexpectedError, we create sarif text correctly", async () => {
const resultsWithUnexpectedError: RunResults = await createResultsWithUnexpectedError();
const formattedText: string = resultsWithUnexpectedError.toFormattedOutput(OutputFormat.SARIF);
const expectedText: string = getContentsOfExpectedOutputFile('unexpectedEngineErrorViolation.goldfile.sarif', true, true);
expect(formattedText).toEqual(expectedText);
});
});

describe("Other misc output formatting tests", () => {
it("When an output format is not supported, then we error", () => {
// This test is just a sanity check in case we add in an output format in the future without updating the
Expand Down
Loading