Skip to content

Commit b42bd9e

Browse files
randi274stephen-carter-at-sf
authored andcommitted
NEW: @W-17916450@ update v5 CLI to allow rules to be saved to JSON (Part 1)
1 parent ad89fdc commit b42bd9e

File tree

17 files changed

+460
-84
lines changed

17 files changed

+460
-84
lines changed

messages/action-summary-viewer.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Configuration written to:
1818

1919
Found 0 rules.
2020

21+
# rules-action.outfile-location
22+
23+
Rules written to:
24+
2125
# rules-action.rules-total
2226

2327
Found %d rule(s) from %d engine(s):

messages/rules-command.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,14 @@ Format to display the rules in the terminal.
102102

103103
# flags.view.description
104104

105-
The format `table` is concise and shows minimal output, the format `detail` shows all available information.
105+
The format `table` is concise and shows minimal output, the format `detail` shows all available information. If you specify neither --view nor --output-file, then the default table view is shown. If you specify --output-file but not --view, only summary information is shown.
106+
107+
# flags.output-file.summary
108+
109+
Output file location to write the selected rules. The file is written in JSON format.
110+
111+
# flags.output-file.description
112+
113+
Use this flag to write the selected rules to a JSON file. For example: "--output-file ./out/rules.json” creates a JSON results file in the "./out" folder. If the file exists already, it will be overwritten.
114+
115+
If you don't specify this flag, the command outputs the rules in the terminal. The view flag can be used in combination with this flag to save the file and output the results.

messages/rules-writer.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# error.unrecognized-file-format
2+
3+
The output file %s has an unsupported extension. Valid extensions include: .json.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"bugs": "https://github.com/forcedotcom/sfdx-scanner/issues",
77
"dependencies": {
88
"@oclif/core": "3.27.0",
9-
"@salesforce/code-analyzer-core": "0.23.0",
9+
"@salesforce/code-analyzer-core": "0.24.0",
1010
"@salesforce/code-analyzer-engine-api": "0.18.0",
1111
"@salesforce/code-analyzer-eslint-engine": "0.20.0",
1212
"@salesforce/code-analyzer-flowtest-engine": "0.18.0",

src/commands/code-analyzer/rules.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {Flags, SfCommand} from '@salesforce/sf-plugins-core';
22
import {View} from '../../Constants';
33
import {CodeAnalyzerConfigFactoryImpl} from '../../lib/factories/CodeAnalyzerConfigFactory';
44
import {EnginePluginsFactoryImpl} from '../../lib/factories/EnginePluginsFactory';
5-
import {RuleDetailDisplayer, RuleTableDisplayer} from '../../lib/viewers/RuleViewer';
5+
import {RuleDetailDisplayer, RulesNoOpDisplayer, RuleTableDisplayer} from '../../lib/viewers/RuleViewer';
66
import {RulesActionSummaryViewer} from '../../lib/viewers/ActionSummaryViewer';
7-
import {RulesAction, RulesDependencies} from '../../lib/actions/RulesAction';
7+
import {RulesAction, RulesDependencies, RulesInput} from '../../lib/actions/RulesAction';
88
import {BundleName, getMessage, getMessages} from '../../lib/messages';
99
import {Displayable, UxDisplay} from '../../lib/Display';
1010
import {LogEventDisplayer} from '../../lib/listeners/LogEventListener';
1111
import {RuleSelectionProgressSpinner} from '../../lib/listeners/ProgressEventListener';
12+
import {CompositeRulesWriter} from '../../lib/writers/RulesWriter';
1213

1314
export default class RulesCommand extends SfCommand<void> implements Displayable {
1415
// We don't need the `--json` output for this command.
@@ -42,11 +43,15 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
4243
char: 'c',
4344
exists: true
4445
}),
46+
'output-file': Flags.file({
47+
summary: getMessage(BundleName.RulesCommand, 'flags.output-file.summary'),
48+
description: getMessage(BundleName.RulesCommand, 'flags.output-file.description'),
49+
char: 'f'
50+
}),
4551
view: Flags.string({
4652
summary: getMessage(BundleName.RulesCommand, 'flags.view.summary'),
4753
description: getMessage(BundleName.RulesCommand, 'flags.view.description'),
4854
char: 'v',
49-
default: View.TABLE,
5055
options: Object.values(View)
5156
})
5257
};
@@ -56,21 +61,53 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
5661
this.warn(getMessage(BundleName.Shared, "warning.command-state", [getMessage(BundleName.Shared, 'label.command-state')]));
5762

5863
const parsedFlags = (await this.parse(RulesCommand)).flags;
59-
const dependencies: RulesDependencies = this.createDependencies(parsedFlags.view as View);
64+
const outputFiles = parsedFlags['output-file'] ? [parsedFlags['output-file']] : [];
65+
const view = parsedFlags.view as View | undefined;
66+
67+
const dependencies: RulesDependencies = this.createDependencies(view, outputFiles);
6068
const action: RulesAction = RulesAction.createAction(dependencies);
61-
await action.execute(parsedFlags);
69+
70+
const rulesInput: RulesInput = {
71+
'config-file': parsedFlags['config-file'],
72+
'output-file': outputFiles,
73+
'rule-selector': parsedFlags['rule-selector'],
74+
'workspace': parsedFlags['workspace'],
75+
};
76+
77+
await action.execute(rulesInput);
6278
}
6379

64-
protected createDependencies(view: View): RulesDependencies {
80+
protected createDependencies(view: View | undefined, outputFiles: string[]): RulesDependencies {
6581
const uxDisplay: UxDisplay = new UxDisplay(this, this.spinner);
66-
return {
82+
const dependencies: RulesDependencies = {
6783
configFactory: new CodeAnalyzerConfigFactoryImpl(),
6884
pluginsFactory: new EnginePluginsFactoryImpl(),
6985
logEventListeners: [new LogEventDisplayer(uxDisplay)],
7086
progressListeners: [new RuleSelectionProgressSpinner(uxDisplay)],
7187
actionSummaryViewer: new RulesActionSummaryViewer(uxDisplay),
72-
viewer: view === View.TABLE ? new RuleTableDisplayer(uxDisplay) : new RuleDetailDisplayer(uxDisplay)
88+
viewer: this.createRulesViewer(view, outputFiles, uxDisplay),
89+
writer: CompositeRulesWriter.fromFiles(outputFiles)
7390
};
91+
92+
return dependencies;
93+
}
94+
95+
/**
96+
* Creates the {@link RuleViewer} that will be called from {@link RulesAction.execute} to display rules.
97+
* If a view option is set, rules will be displayed in the specified format.
98+
* If an output file is set, rules will not display.
99+
* By default, the details display will be used.
100+
*/
101+
private createRulesViewer(view: View | undefined, outputFiles: string[] = [], uxDisplay: UxDisplay) {
102+
if (view === View.DETAIL) {
103+
return new RuleDetailDisplayer(uxDisplay);
104+
} else if (view === View.TABLE) {
105+
return new RuleTableDisplayer(uxDisplay);
106+
} else if (outputFiles.length > 0) {
107+
return new RulesNoOpDisplayer();
108+
}
109+
110+
return new RuleTableDisplayer(uxDisplay);
74111
}
75112
}
76113

src/lib/actions/RulesAction.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import {CodeAnalyzer, CodeAnalyzerConfig, Rule, RuleSelection} from '@salesforce/code-analyzer-core';
2-
import {CodeAnalyzerConfigFactory} from '../factories/CodeAnalyzerConfigFactory';
3-
import {EnginePluginsFactory} from '../factories/EnginePluginsFactory';
4-
import {createWorkspace} from '../utils/WorkspaceUtil';
5-
import {ProgressEventListener} from '../listeners/ProgressEventListener';
6-
import {LogFileWriter} from '../writers/LogWriter';
7-
import {LogEventListener, LogEventLogger} from '../listeners/LogEventListener';
8-
import {RuleViewer} from '../viewers/RuleViewer';
9-
import {RulesActionSummaryViewer} from '../viewers/ActionSummaryViewer';
1+
import { CodeAnalyzer, CodeAnalyzerConfig, Rule, RuleSelection } from '@salesforce/code-analyzer-core';
2+
import { CodeAnalyzerConfigFactory } from '../factories/CodeAnalyzerConfigFactory';
3+
import { EnginePluginsFactory } from '../factories/EnginePluginsFactory';
4+
import { LogEventListener, LogEventLogger } from '../listeners/LogEventListener';
5+
import { ProgressEventListener } from '../listeners/ProgressEventListener';
6+
import { createWorkspace } from '../utils/WorkspaceUtil';
7+
import { RulesActionSummaryViewer } from '../viewers/ActionSummaryViewer';
8+
import { RuleViewer } from '../viewers/RuleViewer';
9+
import { LogFileWriter } from '../writers/LogWriter';
10+
import { RulesWriter } from '../writers/RulesWriter';
1011

1112
export type RulesDependencies = {
1213
configFactory: CodeAnalyzerConfigFactory;
@@ -15,12 +16,15 @@ export type RulesDependencies = {
1516
progressListeners: ProgressEventListener[];
1617
actionSummaryViewer: RulesActionSummaryViewer,
1718
viewer: RuleViewer;
19+
writer: RulesWriter;
1820
}
1921

2022
export type RulesInput = {
2123
'config-file'?: string;
2224
'rule-selector': string[];
25+
'output-file'?: string[];
2326
workspace?: string[];
27+
view?: string;
2428
}
2529

2630
export class RulesAction {
@@ -60,8 +64,14 @@ export class RulesAction {
6064
this.dependencies.logEventListeners.forEach(listener => listener.stopListening());
6165
const rules: Rule[] = core.getEngineNames().flatMap(name => ruleSelection.getRulesFor(name));
6266

67+
this.dependencies.writer.write(ruleSelection)
6368
this.dependencies.viewer.view(rules);
64-
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(ruleSelection, logWriter.getLogDestination());
69+
70+
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(
71+
ruleSelection,
72+
logWriter.getLogDestination(),
73+
input['output-file'] ?? []
74+
);
6575
}
6676

6777
public static createAction(dependencies: RulesDependencies): RulesAction {

src/lib/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum BundleName {
1414
ResultsViewer = 'results-viewer',
1515
RuleViewer = 'rule-viewer',
1616
RulesCommand = 'rules-command',
17+
RulesWriter = 'rules-writer',
1718
ResultsWriter = 'results-writer',
1819
RunAction = 'run-action',
1920
RunCommand = 'run-command',

src/lib/viewers/ActionSummaryViewer.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ abstract class AbstractActionSummaryViewer {
3131
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'common.logfile-location'));
3232
this.display.displayLog(indent(logFile));
3333
}
34+
35+
protected displayOutfiles(outfiles: string[], msgKey: string): void {
36+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, msgKey));
37+
for (const outfile of outfiles) {
38+
this.display.displayLog(indent(outfile));
39+
}
40+
}
3441
}
3542

3643
export class ConfigActionSummaryViewer extends AbstractActionSummaryViewer {
@@ -45,37 +52,39 @@ export class ConfigActionSummaryViewer extends AbstractActionSummaryViewer {
4552
this.displayLineSeparator();
4653

4754
if (outfile) {
48-
this.displayOutfile(outfile);
55+
this.displayOutfiles([outfile], 'config-action.outfile-location');
4956
this.displayLineSeparator();
5057
}
5158

5259
this.displayLogFileInfo(logFile);
5360
}
54-
55-
private displayOutfile(outfile: string): void {
56-
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'config-action.outfile-location'));
57-
this.display.displayLog(indent(outfile));
58-
}
5961
}
6062

6163
export class RulesActionSummaryViewer extends AbstractActionSummaryViewer {
6264
public constructor(display: Display) {
6365
super(display);
6466
}
6567

66-
public viewPostExecutionSummary(ruleSelection: RuleSelection, logFile: string): void {
68+
public viewPostExecutionSummary(ruleSelection: RuleSelection, logFile: string, outfiles: string[]): void {
6769
// Start with separator to cleanly break from anything that's already been logged.
6870
this.displayLineSeparator();
6971
this.displaySummaryHeader();
7072
this.displayLineSeparator();
7173

72-
if (ruleSelection.getCount() === 0) {
74+
const noRulesFound = ruleSelection.getCount() === 0;
75+
76+
if (noRulesFound) {
7377
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'rules-action.found-no-rules'));
7478
} else {
7579
this.displayRuleSelection(ruleSelection);
7680
}
7781
this.displayLineSeparator();
7882

83+
if (outfiles.length > 0) {
84+
this.displayOutfiles(outfiles, 'rules-action.outfile-location');
85+
this.displayLineSeparator();
86+
}
87+
7988
this.displayLogFileInfo(logFile);
8089
}
8190

@@ -107,7 +116,7 @@ export class RunActionSummaryViewer extends AbstractActionSummaryViewer {
107116
this.displayLineSeparator();
108117

109118
if (outfiles.length > 0) {
110-
this.displayOutfiles(outfiles);
119+
this.displayOutfiles(outfiles, 'run-action.outfiles-total');
111120
this.displayLineSeparator();
112121
}
113122

@@ -139,11 +148,4 @@ export class RunActionSummaryViewer extends AbstractActionSummaryViewer {
139148
});
140149
return fileSet.size;
141150
}
142-
143-
private displayOutfiles(outfiles: string[]): void {
144-
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'run-action.outfiles-total'));
145-
for (const outfile of outfiles) {
146-
this.display.displayLog(indent(outfile));
147-
}
148-
}
149151
}

src/lib/viewers/RuleViewer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface RuleViewer {
88
view(rules: Rule[]): void;
99
}
1010

11-
1211
abstract class AbstractRuleDisplayer implements RuleViewer {
1312
protected display: Display;
1413

@@ -111,3 +110,8 @@ export class RuleTableDisplayer extends AbstractRuleDisplayer {
111110
}
112111
}
113112

113+
export class RulesNoOpDisplayer implements RuleViewer {
114+
public view(_rules: Rule[]): void {
115+
return;
116+
}
117+
}

src/lib/writers/RulesWriter.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as fs from 'node:fs';
2+
import { OutputFormat, RuleSelection } from "@salesforce/code-analyzer-core";
3+
import path from "path";
4+
import { BundleName, getMessage } from "../messages";
5+
6+
export interface RulesWriter {
7+
write(rules: RuleSelection): void;
8+
}
9+
10+
11+
export class CompositeRulesWriter implements RulesWriter {
12+
private readonly writers: RulesWriter[] = [];
13+
14+
private constructor(writers: RulesWriter[]) {
15+
this.writers = writers;
16+
}
17+
18+
public write(rules: RuleSelection): void {
19+
this.writers.forEach(w => w.write(rules));
20+
}
21+
22+
public static fromFiles(files: string[]): CompositeRulesWriter {
23+
return new CompositeRulesWriter(files.map(f => new RulesFileWriter(f)));
24+
}
25+
}
26+
27+
export class RulesFileWriter implements RulesWriter {
28+
private readonly file: string;
29+
private readonly format: OutputFormat;
30+
31+
public constructor(file: string) {
32+
this.file = file;
33+
const ext = path.extname(file).toLowerCase();
34+
35+
if (ext === '.json') {
36+
this.format = OutputFormat.JSON;
37+
} else {
38+
throw new Error(getMessage(BundleName.RulesWriter, 'error.unrecognized-file-format', [file]));
39+
}
40+
}
41+
42+
public write(ruleSelection: RuleSelection): void {
43+
const contents = ruleSelection.toFormattedOutput(this.format);
44+
fs.writeFileSync(this.file, contents);
45+
}
46+
}

0 commit comments

Comments
 (0)