Skip to content

Commit 0ed0a9e

Browse files
authored
NEW @W-17977844@ rules command can output csv files (#1877)
1 parent 0225c47 commit 0ed0a9e

File tree

9 files changed

+113
-74
lines changed

9 files changed

+113
-74
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ charset = utf-8
77
trim_trailing_whitespace = true
88
insert_final_newline = true
99

10+
[*.ts]
11+
indent_style = tab
12+
indent_size = 4
13+
1014
[*.java]
1115
indent_style = space
1216
indent_size = 4

messages/rules-command.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,15 @@ If you specify neither `--view` nor `--output-file`, then the default table view
124124

125125
# flags.output-file.summary
126126

127-
Name of the file where the selected rules are written. The file format depends on the extension you specify; currently, only .json is supported for JSON-formatted output.
127+
Name of the file where the selected rules are written. The file format depends on the extension you specify; the currently supported extensions are .json and .csv
128128

129129
# flags.output-file.description
130130

131-
If you specify a file within folder, such as `--output-file ./out/rules.json`, the folder must already exist, or you get an error. If the file already exists, it's overwritten without prompting.
131+
If you don't specify this flag, the command outputs the rules to only the terminal. Use this flag to write the rules to a file; the format of the rules depends on the extension you provide. For example, `--output-file rules.csv` creates a comma-separated values file. You can specify one of these extensions:
132+
133+
- .csv
134+
- .json
132135

133-
If you don't specify this flag, the command outputs the rules to only the terminal.
136+
To output the rules to multiple files, specify this flag multiple times. For example, `--output-file rules.json --output-file rules.csv` creates both a JSON file and a CSV file.
137+
138+
If you specify a file within folder, such as `--output-file ./out/rules.json`, the folder must already exist, or you get an error. If the file already exists, it's overwritten without prompting.

messages/rules-writer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# error.unrecognized-file-format
22

3-
The output file %s has an unsupported extension. Valid extension(s): .json.
3+
The output file %s has an unsupported extension. Valid extension(s): .json, .csv

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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/code-analyzer/issues",
77
"dependencies": {
88
"@oclif/core": "3.27.0",
9-
"@salesforce/code-analyzer-core": "0.32.0",
9+
"@salesforce/code-analyzer-core": "0.33.0",
1010
"@salesforce/code-analyzer-engine-api": "0.27.0",
1111
"@salesforce/code-analyzer-eslint-engine": "0.29.0",
1212
"@salesforce/code-analyzer-flow-engine": "0.25.0",

src/commands/code-analyzer/rules.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
4848
char: 'c',
4949
exists: true
5050
}),
51-
'output-file': Flags.file({
51+
'output-file': Flags.string({
5252
summary: getMessage(BundleName.RulesCommand, 'flags.output-file.summary'),
5353
description: getMessage(BundleName.RulesCommand, 'flags.output-file.description'),
54-
char: 'f'
54+
char: 'f',
55+
multiple: true,
56+
delimiter: ','
5557
}),
5658
view: Flags.string({
5759
summary: getMessage(BundleName.RulesCommand, 'flags.view.summary'),
@@ -63,7 +65,7 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
6365

6466
public async run(): Promise<void> {
6567
const parsedFlags = (await this.parse(RulesCommand)).flags;
66-
const outputFiles = parsedFlags['output-file'] ? [parsedFlags['output-file']] : [];
68+
const outputFiles = parsedFlags['output-file'] ?? [];
6769
const view = parsedFlags.view as View | undefined;
6870

6971
const dependencies: RulesDependencies = this.createDependencies(view, outputFiles);

src/lib/writers/RulesWriter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ export class RulesFileWriter implements RulesWriter {
3333
const ext = path.extname(file).toLowerCase();
3434

3535
if (ext === '.json') {
36-
this.format = OutputFormat.JSON;
36+
this.format = OutputFormat.JSON;
37+
} else if (ext === '.csv') {
38+
this.format = OutputFormat.CSV;
3739
} else {
3840
throw new Error(getMessage(BundleName.RulesWriter, 'error.unrecognized-file-format', [file]));
3941
}
4042
}
41-
43+
4244
public write(ruleSelection: RuleSelection): void {
4345
const contents = ruleSelection.toFormattedOutput(this.format);
4446
fs.writeFileSync(this.file, contents);

test/commands/code-analyzer/rules.test.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@ describe('`code-analyzer rules` tests', () => {
117117

118118
describe('--output-file', () => {
119119

120-
const inputValue1 = path.join('my', 'rules-output.json');
121-
const inputValue2 = path.join('my', 'second', 'rules-output.json');
120+
const inputValue1 = path.join('my', 'first', 'rules-output.json');
121+
const inputValue2 = path.join('my', 'second', 'rules-output.csv');
122+
const inputValue3 = path.join('my', 'third', 'rules-output.json');
123+
const inputValue4 = path.join('my', 'fourth', 'rules-output.csv');
122124

123-
it('Accepts one file path', async () => {
125+
it('Can be supplied once with a single value', async () => {
124126
await RulesCommand.run(['--output-file', inputValue1]);
125127
expect(executeSpy).toHaveBeenCalled();
126128
expect(createActionSpy).toHaveBeenCalled();
@@ -129,10 +131,31 @@ describe('`code-analyzer rules` tests', () => {
129131
expect(receivedFiles).toEqual([inputValue1]);
130132
});
131133

132-
it('Can only be supplied once', async () => {
133-
const executionPromise = RulesCommand.run(['--output-file', inputValue1, '--output-file', inputValue2]);
134-
await expect(executionPromise).rejects.toThrow(`Flag --output-file can only be specified once`);
135-
expect(executeSpy).not.toHaveBeenCalled();
134+
it('Can be supplied once with multiple comma-separated values', async () => {
135+
await RulesCommand.run(['--output-file', `${inputValue1},${inputValue2}`]);
136+
expect(executeSpy).toHaveBeenCalled();
137+
expect(createActionSpy).toHaveBeenCalled();
138+
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2]);
139+
expect(fromFilesSpy).toHaveBeenCalled()
140+
expect(receivedFiles).toEqual([inputValue1, inputValue2]);
141+
});
142+
143+
it('Can be supplied multiple times with one value each', async () => {
144+
await RulesCommand.run(['--output-file', inputValue1, '--output-file', inputValue2]);
145+
expect(executeSpy).toHaveBeenCalled();
146+
expect(createActionSpy).toHaveBeenCalled();
147+
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2]);
148+
expect(fromFilesSpy).toHaveBeenCalled()
149+
expect(receivedFiles).toEqual([inputValue1, inputValue2]);
150+
});
151+
152+
it('Can be supplied multiple times with multiple comma-separated values', async () => {
153+
await RulesCommand.run(['--output-file', `${inputValue1},${inputValue2}`, '--output-file', `${inputValue3},${inputValue4}`]);
154+
expect(executeSpy).toHaveBeenCalled();
155+
expect(createActionSpy).toHaveBeenCalled();
156+
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2, inputValue3, inputValue4]);
157+
expect(fromFilesSpy).toHaveBeenCalled()
158+
expect(receivedFiles).toEqual([inputValue1, inputValue2, inputValue3, inputValue4]);
136159
});
137160

138161
it('Can be referenced by its shortname, -f', async () => {

test/lib/writers/RulesWriter.test.ts

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,68 @@ import * as Stub from '../../stubs/StubRuleSelection';
66

77
describe('RulesWriter', () => {
88

9-
let writeFileSpy: jest.SpyInstance;
10-
let writeFileInvocations: { file: fs.PathOrFileDescriptor, contents: string | ArrayBufferView }[];
9+
let writeFileSpy: jest.SpyInstance;
10+
let writeFileInvocations: { file: fs.PathOrFileDescriptor, contents: string | ArrayBufferView }[];
1111

12-
beforeEach(() => {
13-
jest.resetAllMocks();
14-
writeFileInvocations = [];
15-
writeFileSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((file, contents) => {
16-
writeFileInvocations.push({file, contents});
17-
});
18-
});
12+
beforeEach(() => {
13+
jest.resetAllMocks();
14+
writeFileInvocations = [];
15+
writeFileSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((file, contents) => {
16+
writeFileInvocations.push({file, contents});
17+
});
18+
});
1919

20-
describe('RulesFileWriter', () => {
20+
describe('RulesFileWriter', () => {
2121

22-
it('Rejects invalid file format', () => {
23-
const invalidFile = 'file.xml';
24-
expect(() => new RulesFileWriter(invalidFile)).toThrow(invalidFile);
25-
});
22+
it('Rejects invalid file format', () => {
23+
const invalidFile = 'file.xml';
24+
expect(() => new RulesFileWriter(invalidFile)).toThrow(invalidFile);
25+
});
2626

27-
it('Writes to a json file path', () => {
28-
const outfilePath = path.join('the', 'results', 'path', 'file.json');
29-
const expectations = {
30-
file: outfilePath,
31-
contents: `Rules formatted as ${OutputFormat.JSON}`
32-
};
33-
const rulesWriter = new RulesFileWriter(expectations.file);
34-
const stubbedSelection = new Stub.StubEmptyRuleSelection();
35-
rulesWriter.write(stubbedSelection);
27+
it.each([
28+
{ext: 'json', format: OutputFormat.JSON},
29+
{ext: 'csv', format: OutputFormat.CSV}
30+
])('Writes to a $ext file path', ({ext, format}) => {
31+
const outfilePath = path.join('the', 'results', 'path', `file.${ext}`);
32+
const expectations = {
33+
file: outfilePath,
34+
contents: `Rules formatted as ${format}`
35+
};
36+
const rulesWriter = new RulesFileWriter(expectations.file);
37+
const stubbedSelection = new Stub.StubEmptyRuleSelection();
38+
rulesWriter.write(stubbedSelection);
3639

37-
expect(writeFileSpy).toHaveBeenCalled();
38-
expect(writeFileInvocations).toEqual([expectations]);
39-
});
40-
});
40+
expect(writeFileSpy).toHaveBeenCalled();
41+
expect(writeFileInvocations).toEqual([expectations]);
42+
});
43+
});
4144

42-
describe('CompositeRulesWriter', () => {
45+
describe('CompositeRulesWriter', () => {
4346

44-
it('Does a no-op when there are no files to write to', () => {
45-
const outputFileWriter = CompositeRulesWriter.fromFiles([]);
46-
const stubbedEmptyRuleSelection = new Stub.StubEmptyRuleSelection();
47+
it('Does a no-op when there are no files to write to', () => {
48+
const outputFileWriter = CompositeRulesWriter.fromFiles([]);
49+
const stubbedEmptyRuleSelection = new Stub.StubEmptyRuleSelection();
4750

48-
outputFileWriter.write(stubbedEmptyRuleSelection);
51+
outputFileWriter.write(stubbedEmptyRuleSelection);
4952

50-
expect(writeFileSpy).not.toHaveBeenCalled();
51-
});
52-
53-
it('When given multiple files, outputs to all of them', () => {
54-
const expectations = [{
55-
file: 'outFile1.json',
56-
contents: `Rules formatted as ${OutputFormat.JSON}`
57-
}, {
58-
file: 'outFile2.json',
59-
contents: `Rules formatted as ${OutputFormat.JSON}`
60-
}];
61-
const outputFileWriter = CompositeRulesWriter.fromFiles(expectations.map(i => i.file));
62-
const stubbedSelection = new Stub.StubEmptyRuleSelection();
63-
64-
outputFileWriter.write(stubbedSelection);
65-
66-
expect(writeFileSpy).toHaveBeenCalledTimes(2);
67-
expect(writeFileInvocations).toEqual(expectations);
68-
});
69-
})
70-
});
53+
expect(writeFileSpy).not.toHaveBeenCalled();
54+
});
55+
56+
it('When given multiple files, outputs to all of them', () => {
57+
const expectations = [{
58+
file: 'outFile1.json',
59+
contents: `Rules formatted as ${OutputFormat.JSON}`
60+
}, {
61+
file: 'outFile2.json',
62+
contents: `Rules formatted as ${OutputFormat.JSON}`
63+
}];
64+
const outputFileWriter = CompositeRulesWriter.fromFiles(expectations.map(i => i.file));
65+
const stubbedSelection = new Stub.StubEmptyRuleSelection();
66+
67+
outputFileWriter.write(stubbedSelection);
68+
69+
expect(writeFileSpy).toHaveBeenCalledTimes(2);
70+
expect(writeFileInvocations).toEqual(expectations);
71+
});
72+
})
73+
});

0 commit comments

Comments
 (0)