Skip to content

Commit

Permalink
Merge pull request #1307 from forcedotcom/d/W-14689342
Browse files Browse the repository at this point in the history
CHANGE (Other): @W-14689342@: Add environment variable to output to an internal outfile
  • Loading branch information
stephen-carter-at-sf authored Jan 4, 2024
2 parents 6d7dd2b + 12502fd commit a7d4a8e
Show file tree
Hide file tree
Showing 21 changed files with 521 additions and 148 deletions.
8 changes: 8 additions & 0 deletions messages/run-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ throw an error when a violation threshold is reached, the --normalize-severity i

Throws an error when violations are found with equal or greater severity than the provided value. Values are 1 (high), 2 (moderate), and 3 (low). Exit code is the most severe violation. Using this flag also invokes the --normalize-severity flag.

# internal.outfileMustBeValid

The %s environment variable must be a well-formed filepath.

# internal.outfileMustBeSupportedType

The %s environment variable must be of a supported type: .csv; .xml; .json; .html; .sarif.

# validations.cannotWriteTableToFile

Format 'table' can't be written to a file. Specify a different format.
Expand Down
3 changes: 2 additions & 1 deletion src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const PMD_CATALOG_FILE = 'PmdCatalog.json';
// TODO: We should flesh this one-off solution out into one that handles all the various env vars we use.
// E.g., the ones defined in `EnvironmentVariable.ts` and `dfa.ts`.
export const ENV_VAR_NAMES = {
SCANNER_PATH_OVERRIDE: 'SCANNER_PATH_OVERRIDE'
SCANNER_PATH_OVERRIDE: 'SCANNER_PATH_OVERRIDE',
SCANNER_INTERNAL_OUTFILE: 'SCANNER_INTERNAL_OUTFILE'
};

export const INTERNAL_ERROR_CODE = 1;
Expand Down
5 changes: 4 additions & 1 deletion src/commands/scanner/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {Action} from "../../lib/ScannerCommand";
import {Display} from "../../lib/Display";
import {RunAction} from "../../lib/actions/RunAction";
import {RuleFilterFactory, RuleFilterFactoryImpl} from "../../lib/RuleFilterFactory";
import {ResultsProcessorFactory, ResultsProcessorFactoryImpl} from "../../lib/output/ResultsProcessorFactory";

/**
* Defines the "run" command for the "scanner" cli.
Expand Down Expand Up @@ -91,6 +92,8 @@ export default class Run extends ScannerRunCommand {
const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version);
const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl();
const engineOptionsFactory: EngineOptionsFactory = new RunEngineOptionsFactory(inputProcessor);
return new RunAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory);
const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl();
return new RunAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory,
resultsProcessorFactory);
}
}
5 changes: 4 additions & 1 deletion src/commands/scanner/run/dfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Display} from "../../../lib/Display";
import {Action} from "../../../lib/ScannerCommand";
import {RunDfaAction} from "../../../lib/actions/RunDfaAction";
import {RuleFilterFactory, RuleFilterFactoryImpl} from "../../../lib/RuleFilterFactory";
import {ResultsProcessorFactory, ResultsProcessorFactoryImpl} from "../../../lib/output/ResultsProcessorFactory";

/**
* Defines the "run dfa" command for the "scanner" cli.
Expand Down Expand Up @@ -79,6 +80,8 @@ export default class Dfa extends ScannerRunCommand {
const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version);
const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl();
const engineOptionsFactory: EngineOptionsFactory = new RunDfaEngineOptionsFactory(inputProcessor);
return new RunDfaAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory);
const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl();
return new RunDfaAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory,
resultsProcessorFactory);
}
}
3 changes: 2 additions & 1 deletion src/lib/InputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import normalize = require('normalize-path');
import path = require('path');
import untildify = require("untildify");
import {RunOptions} from "./RuleManager";
import {RunOutputOptions} from "./util/RunResultsProcessor";
import {RunOutputOptions} from "./output/RunResultsProcessor";
import {inferFormatFromOutfile, OutputFormat} from "./output/OutputFormat";

/**
Expand Down Expand Up @@ -62,6 +62,7 @@ export class InputProcessorImpl implements InputProcessor {
public createRunOutputOptions(inputs: Inputs): RunOutputOptions {
return {
format: outputFormatFromInputs(inputs),
verboseViolations: inputs["verbose-violations"] as boolean,
severityForError: inputs['severity-threshold'] as number,
outfile: inputs.outfile as string
};
Expand Down
18 changes: 14 additions & 4 deletions src/lib/actions/AbstractRunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {Display} from "../Display";
import {RuleFilter} from "../RuleFilter";
import {RuleFilterFactory} from "../RuleFilterFactory";
import {Controller} from "../../Controller";
import {RunOutputOptions, RunResultsProcessor} from "../util/RunResultsProcessor";
import {RunOutputOptions} from "../output/RunResultsProcessor";
import {InputProcessor} from "../InputProcessor";
import {EngineOptionsFactory} from "../EngineOptionsFactory";
import {INTERNAL_ERROR_CODE} from "../../Constants";
import {Results} from "../output/Results";
import {inferFormatFromOutfile, OutputFormat} from "../output/OutputFormat";
import {ResultsProcessor} from "../output/ResultsProcessor";
import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory";
import {JsonReturnValueHolder} from "../output/JsonReturnValueHolder";

/**
* Abstract Action to share a common implementation behind the "run" and "run dfa" commands
Expand All @@ -26,14 +29,17 @@ export abstract class AbstractRunAction implements Action {
private readonly inputProcessor: InputProcessor;
private readonly ruleFilterFactory: RuleFilterFactory;
private readonly engineOptionsFactory: EngineOptionsFactory;
private readonly resultsProcessorFactory: ResultsProcessorFactory;

protected constructor(logger: Logger, display: Display, inputProcessor: InputProcessor,
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) {
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory,
resultsProcessorFactory: ResultsProcessorFactory) {
this.logger = logger;
this.display = display;
this.inputProcessor = inputProcessor;
this.ruleFilterFactory = ruleFilterFactory;
this.engineOptionsFactory = engineOptionsFactory;
this.resultsProcessorFactory = resultsProcessorFactory;
}

protected abstract isDfa(): boolean;
Expand Down Expand Up @@ -78,14 +84,18 @@ export abstract class AbstractRunAction implements Action {
const engineOptions: EngineOptions = this.engineOptionsFactory.createEngineOptions(inputs);
const outputOptions: RunOutputOptions = this.inputProcessor.createRunOutputOptions(inputs);

const jsonReturnValueHolder: JsonReturnValueHolder = new JsonReturnValueHolder();
const resultsProcessor: ResultsProcessor = this.resultsProcessorFactory.createResultsProcessor(
this.display, outputOptions, jsonReturnValueHolder);

// TODO: Inject RuleManager as a dependency to improve testability by removing coupling to runtime implementation
const ruleManager: RuleManager = await Controller.createRuleManager();

try {
const results: Results = await ruleManager.runRulesMatchingCriteria(filters, targetPaths, runOptions, engineOptions);
this.logger.trace(`Processing output with format ${outputOptions.format}`);
return new RunResultsProcessor(this.display, outputOptions, inputs["verbose-violations"] as boolean)
.processResults(results);
await resultsProcessor.processResults(results);
return jsonReturnValueHolder.get();

} catch (e) {
// Rethrow any errors as SF errors.
Expand Down
6 changes: 4 additions & 2 deletions src/lib/actions/RunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import {Inputs} from "../../types";
import {Logger, SfError} from "@salesforce/core";
import {BundleName, getMessage} from "../../MessageCatalog";
import {RuleFilterFactory} from "../RuleFilterFactory";
import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory";

/**
* The Action behind the "run" command
*/
export class RunAction extends AbstractRunAction {
public constructor(logger: Logger, display: Display, inputProcessor: InputProcessor,
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) {
super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory);
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory,
resultsProcessorFactory: ResultsProcessorFactory) {
super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, resultsProcessorFactory);
}

public override async validateInputs(inputs: Inputs): Promise<void> {
Expand Down
6 changes: 4 additions & 2 deletions src/lib/actions/RunDfaAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {FileHandler} from "../util/FileHandler";
import {Logger, SfError} from "@salesforce/core";
import {BundleName, getMessage} from "../../MessageCatalog";
import * as globby from "globby";
import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory";

/**
* The Action behind the "run dfa" command
*/
export class RunDfaAction extends AbstractRunAction {
public constructor(logger: Logger, display: Display, inputProcessor: InputProcessor,
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) {
super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory);
ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory,
resultsProcessorFactory: ResultsProcessorFactory) {
super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, resultsProcessorFactory);
}

public override async validateInputs(inputs: Inputs): Promise<void> {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/output/JsonReturnValueHolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {AnyJson} from "@salesforce/ts-types";

/**
* Container to hold the json return value for the --json flag used by some of the cli commands
*/
export class JsonReturnValueHolder {
private jsonReturnValue: AnyJson;

public set(jsonReturnValue: AnyJson): void {
this.jsonReturnValue = jsonReturnValue;
}

public get(): AnyJson {
return this.jsonReturnValue;
}
}
24 changes: 24 additions & 0 deletions src/lib/output/OutfileResultsProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {ResultsProcessor} from "./ResultsProcessor";
import {Results} from "./Results";
import {OutputFormat} from "./OutputFormat";
import {FormattedOutput} from "../../types";
import {FileHandler} from "../util/FileHandler";

/**
* Processes results to produce an output file
*/
export class OutfileResultsProcessor implements ResultsProcessor {
private readonly outputFormat: OutputFormat;
private readonly outfile: string;
private readonly verboseViolations: boolean;
public constructor(format:OutputFormat, outfile: string, verboseViolations: boolean) {
this.outputFormat = format;
this.outfile = outfile;
this.verboseViolations = verboseViolations;
}

public async processResults(results: Results): Promise<void> {
const fileContents: FormattedOutput = await results.toFormattedOutput(this.outputFormat, this.verboseViolations);
(new FileHandler()).writeFileSync(this.outfile, fileContents as string);
}
}
18 changes: 15 additions & 3 deletions src/lib/output/OutputFormat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {SfError} from "@salesforce/core";
import {BundleName, getMessage} from "../../MessageCatalog";
import {INTERNAL_ERROR_CODE} from "../../Constants";
import {ENV_VAR_NAMES, INTERNAL_ERROR_CODE} from "../../Constants";

export enum OutputFormat {
CSV = 'csv',
Expand All @@ -13,9 +13,21 @@ export enum OutputFormat {
}

export function inferFormatFromOutfile(outfile: string): OutputFormat {
return determineOutputFormat(outfile,
getMessage(BundleName.CommonRun, 'validations.outfileMustBeValid'),
getMessage(BundleName.CommonRun, 'validations.outfileMustBeSupportedType'))
}

export function inferFormatFromInternalOutfile(outfile: string): OutputFormat {
return determineOutputFormat(outfile,
getMessage(BundleName.CommonRun, 'internal.outfileMustBeValid', [ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE]),
getMessage(BundleName.CommonRun, 'internal.outfileMustBeSupportedType', [ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE]));
}

function determineOutputFormat(outfile: string, invalidFileMsg: string, invalidExtensionMsg: string): OutputFormat {
const lastPeriod: number = outfile.lastIndexOf('.');
if (lastPeriod < 1 || lastPeriod + 1 === outfile.length) {
throw new SfError(getMessage(BundleName.CommonRun, 'validations.outfileMustBeValid'), null, null, INTERNAL_ERROR_CODE);
throw new SfError(invalidFileMsg, null, null, INTERNAL_ERROR_CODE);
}
const fileExtension: string = outfile.slice(lastPeriod + 1).toLowerCase();
switch (fileExtension) {
Expand All @@ -26,6 +38,6 @@ export function inferFormatFromOutfile(outfile: string): OutputFormat {
case OutputFormat.XML:
return fileExtension;
default:
throw new SfError(getMessage(BundleName.CommonRun, 'validations.outfileMustBeSupportedType'), null, null, INTERNAL_ERROR_CODE);
throw new SfError(invalidExtensionMsg, null, null, INTERNAL_ERROR_CODE);
}
}
10 changes: 9 additions & 1 deletion src/lib/output/Results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface Results {
export class RunResults implements Results {
private readonly ruleResults: RuleResult[];
private readonly executedEngines: Set<string>;
private readonly formattedResultsCache: Map<string, FormattedOutput> = new Map;

constructor(ruleResults: RuleResult[], executedEngines: Set<string>, ) {
this.ruleResults = ruleResults;
Expand Down Expand Up @@ -100,6 +101,11 @@ export class RunResults implements Results {
}

public async toFormattedOutput(format: OutputFormat, verboseViolations: boolean): Promise<FormattedOutput> {
const cacheKey: string = String(format) + '_' + String(verboseViolations);
if (this.formattedResultsCache.has(cacheKey)) {
return Promise.resolve(this.formattedResultsCache.get(cacheKey));
}

let outputFormatter: OutputFormatter;
switch (format) {
case OutputFormat.CSV:
Expand All @@ -126,6 +132,8 @@ export class RunResults implements Results {
default:
throw new SfError('Unrecognized output format.');
}
return await outputFormatter.format(this);
const formattedOutput: FormattedOutput = await outputFormatter.format(this);
this.formattedResultsCache.set(cacheKey, formattedOutput);
return formattedOutput;
}
}
25 changes: 25 additions & 0 deletions src/lib/output/ResultsProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Results} from "./Results";

/**
* Interface to process run results
*/
export interface ResultsProcessor {
processResults(results: Results): Promise<void>;
}

/**
* A composite results processor
*/
export class CompositeResultsProcessor implements ResultsProcessor {
private readonly delegates: ResultsProcessor[];

public constructor(delegateResultsProcessors: ResultsProcessor[]) {
this.delegates = delegateResultsProcessors;
}

async processResults(results: Results): Promise<void> {
for (const delegate of this.delegates) {
await delegate.processResults(results);
}
}
}
35 changes: 35 additions & 0 deletions src/lib/output/ResultsProcessorFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Display} from "../Display";
import {JsonReturnValueHolder} from "./JsonReturnValueHolder";
import {RunOutputOptions, RunResultsProcessor} from "./RunResultsProcessor";
import {CompositeResultsProcessor, ResultsProcessor} from "./ResultsProcessor";
import {ENV_VAR_NAMES} from "../../Constants";
import {inferFormatFromInternalOutfile, OutputFormat} from "./OutputFormat";
import {OutfileResultsProcessor} from "./OutfileResultsProcessor";

/**
* Interface for creating a ResultsProcessor
*/
export interface ResultsProcessorFactory {
createResultsProcessor(display: Display, runOutputOptions: RunOutputOptions,
jsonReturnValueHolder: JsonReturnValueHolder): ResultsProcessor
}

/**
* Runtime implementation of the ResultsProcessorFactory interface
*/
export class ResultsProcessorFactoryImpl implements ResultsProcessorFactory {
public createResultsProcessor(display: Display, runOutputOptions: RunOutputOptions,
jsonReturnValueHolder: JsonReturnValueHolder): ResultsProcessor {
const resultsProcessors: ResultsProcessor[] = [new RunResultsProcessor(display, runOutputOptions, jsonReturnValueHolder)];
this.addProcessorForInternalOutfileIfNeeded(resultsProcessors, runOutputOptions.verboseViolations);
return new CompositeResultsProcessor(resultsProcessors);
}

private addProcessorForInternalOutfileIfNeeded(resultsProcessors: ResultsProcessor[], verboseViolations: boolean): void {
const internalOutfile: string = process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE];
if (internalOutfile && internalOutfile.length > 0) {
const internalOutputFormat: OutputFormat = inferFormatFromInternalOutfile(internalOutfile);
resultsProcessors.push(new OutfileResultsProcessor(internalOutputFormat, internalOutfile, verboseViolations));
}
}
}
Loading

0 comments on commit a7d4a8e

Please sign in to comment.