Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.ts]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticing that some of the files created by people other than me are space-indented, so I'm adding a thing to the .editorconfig to hopefully keep it all consistent in the future.

indent_style = tab
indent_size = 4

[*.java]
indent_style = space
indent_size = 4
Expand Down
11 changes: 8 additions & 3 deletions messages/rules-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,15 @@ If you specify neither `--view` nor `--output-file`, then the default table view

# flags.output-file.summary

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.
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

# flags.output-file.description

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.
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:

- .csv
- .json

If you don't specify this flag, the command outputs the rules to only the terminal.
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.

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.
2 changes: 1 addition & 1 deletion messages/rules-writer.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# error.unrecognized-file-format

The output file %s has an unsupported extension. Valid extension(s): .json.
The output file %s has an unsupported extension. Valid extension(s): .json, .csv
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"bugs": "https://github.com/forcedotcom/code-analyzer/issues",
"dependencies": {
"@oclif/core": "3.27.0",
"@salesforce/code-analyzer-core": "0.32.0",
"@salesforce/code-analyzer-core": "0.33.0",
"@salesforce/code-analyzer-engine-api": "0.27.0",
"@salesforce/code-analyzer-eslint-engine": "0.29.0",
"@salesforce/code-analyzer-flow-engine": "0.25.0",
Expand Down
8 changes: 5 additions & 3 deletions src/commands/code-analyzer/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
char: 'c',
exists: true
}),
'output-file': Flags.file({
'output-file': Flags.string({
summary: getMessage(BundleName.RulesCommand, 'flags.output-file.summary'),
description: getMessage(BundleName.RulesCommand, 'flags.output-file.description'),
char: 'f'
char: 'f',
multiple: true,
delimiter: ','
}),
view: Flags.string({
summary: getMessage(BundleName.RulesCommand, 'flags.view.summary'),
Expand All @@ -63,7 +65,7 @@ export default class RulesCommand extends SfCommand<void> implements Displayable

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

const dependencies: RulesDependencies = this.createDependencies(view, outputFiles);
Expand Down
6 changes: 4 additions & 2 deletions src/lib/writers/RulesWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ export class RulesFileWriter implements RulesWriter {
const ext = path.extname(file).toLowerCase();

if (ext === '.json') {
this.format = OutputFormat.JSON;
this.format = OutputFormat.JSON;
} else if (ext === '.csv') {
this.format = OutputFormat.CSV;
} else {
throw new Error(getMessage(BundleName.RulesWriter, 'error.unrecognized-file-format', [file]));
}
}

public write(ruleSelection: RuleSelection): void {
const contents = ruleSelection.toFormattedOutput(this.format);
fs.writeFileSync(this.file, contents);
Expand Down
37 changes: 30 additions & 7 deletions test/commands/code-analyzer/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ describe('`code-analyzer rules` tests', () => {

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

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

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

it('Can only be supplied once', async () => {
const executionPromise = RulesCommand.run(['--output-file', inputValue1, '--output-file', inputValue2]);
await expect(executionPromise).rejects.toThrow(`Flag --output-file can only be specified once`);
expect(executeSpy).not.toHaveBeenCalled();
it('Can be supplied once with multiple comma-separated values', async () => {
await RulesCommand.run(['--output-file', `${inputValue1},${inputValue2}`]);
expect(executeSpy).toHaveBeenCalled();
expect(createActionSpy).toHaveBeenCalled();
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2]);
expect(fromFilesSpy).toHaveBeenCalled()
expect(receivedFiles).toEqual([inputValue1, inputValue2]);
});

it('Can be supplied multiple times with one value each', async () => {
await RulesCommand.run(['--output-file', inputValue1, '--output-file', inputValue2]);
expect(executeSpy).toHaveBeenCalled();
expect(createActionSpy).toHaveBeenCalled();
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2]);
expect(fromFilesSpy).toHaveBeenCalled()
expect(receivedFiles).toEqual([inputValue1, inputValue2]);
});

it('Can be supplied multiple times with multiple comma-separated values', async () => {
await RulesCommand.run(['--output-file', `${inputValue1},${inputValue2}`, '--output-file', `${inputValue3},${inputValue4}`]);
expect(executeSpy).toHaveBeenCalled();
expect(createActionSpy).toHaveBeenCalled();
expect(receivedActionInput).toHaveProperty('output-file', [inputValue1, inputValue2, inputValue3, inputValue4]);
expect(fromFilesSpy).toHaveBeenCalled()
expect(receivedFiles).toEqual([inputValue1, inputValue2, inputValue3, inputValue4]);
});

it('Can be referenced by its shortname, -f', async () => {
Expand Down
109 changes: 56 additions & 53 deletions test/lib/writers/RulesWriter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,68 @@ import * as Stub from '../../stubs/StubRuleSelection';

describe('RulesWriter', () => {

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

beforeEach(() => {
jest.resetAllMocks();
writeFileInvocations = [];
writeFileSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((file, contents) => {
writeFileInvocations.push({file, contents});
});
});
beforeEach(() => {
jest.resetAllMocks();
writeFileInvocations = [];
writeFileSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((file, contents) => {
writeFileInvocations.push({file, contents});
});
});

describe('RulesFileWriter', () => {
describe('RulesFileWriter', () => {

it('Rejects invalid file format', () => {
const invalidFile = 'file.xml';
expect(() => new RulesFileWriter(invalidFile)).toThrow(invalidFile);
});
it('Rejects invalid file format', () => {
const invalidFile = 'file.xml';
expect(() => new RulesFileWriter(invalidFile)).toThrow(invalidFile);
});

it('Writes to a json file path', () => {
const outfilePath = path.join('the', 'results', 'path', 'file.json');
const expectations = {
file: outfilePath,
contents: `Rules formatted as ${OutputFormat.JSON}`
};
const rulesWriter = new RulesFileWriter(expectations.file);
const stubbedSelection = new Stub.StubEmptyRuleSelection();
rulesWriter.write(stubbedSelection);
it.each([
{ext: 'json', format: OutputFormat.JSON},
{ext: 'csv', format: OutputFormat.CSV}
])('Writes to a $ext file path', ({ext, format}) => {
const outfilePath = path.join('the', 'results', 'path', `file.${ext}`);
const expectations = {
file: outfilePath,
contents: `Rules formatted as ${format}`
};
const rulesWriter = new RulesFileWriter(expectations.file);
const stubbedSelection = new Stub.StubEmptyRuleSelection();
rulesWriter.write(stubbedSelection);

expect(writeFileSpy).toHaveBeenCalled();
expect(writeFileInvocations).toEqual([expectations]);
});
});
expect(writeFileSpy).toHaveBeenCalled();
expect(writeFileInvocations).toEqual([expectations]);
});
Comment on lines +27 to +42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this test to use it.each(), and that's the only functional change. Everything else was just whitespace to make it tab-indented.

});

describe('CompositeRulesWriter', () => {
describe('CompositeRulesWriter', () => {

it('Does a no-op when there are no files to write to', () => {
const outputFileWriter = CompositeRulesWriter.fromFiles([]);
const stubbedEmptyRuleSelection = new Stub.StubEmptyRuleSelection();
it('Does a no-op when there are no files to write to', () => {
const outputFileWriter = CompositeRulesWriter.fromFiles([]);
const stubbedEmptyRuleSelection = new Stub.StubEmptyRuleSelection();

outputFileWriter.write(stubbedEmptyRuleSelection);
outputFileWriter.write(stubbedEmptyRuleSelection);

expect(writeFileSpy).not.toHaveBeenCalled();
});
it('When given multiple files, outputs to all of them', () => {
const expectations = [{
file: 'outFile1.json',
contents: `Rules formatted as ${OutputFormat.JSON}`
}, {
file: 'outFile2.json',
contents: `Rules formatted as ${OutputFormat.JSON}`
}];
const outputFileWriter = CompositeRulesWriter.fromFiles(expectations.map(i => i.file));
const stubbedSelection = new Stub.StubEmptyRuleSelection();
outputFileWriter.write(stubbedSelection);
expect(writeFileSpy).toHaveBeenCalledTimes(2);
expect(writeFileInvocations).toEqual(expectations);
});
})
});
expect(writeFileSpy).not.toHaveBeenCalled();
});

it('When given multiple files, outputs to all of them', () => {
const expectations = [{
file: 'outFile1.json',
contents: `Rules formatted as ${OutputFormat.JSON}`
}, {
file: 'outFile2.json',
contents: `Rules formatted as ${OutputFormat.JSON}`
}];
const outputFileWriter = CompositeRulesWriter.fromFiles(expectations.map(i => i.file));
const stubbedSelection = new Stub.StubEmptyRuleSelection();

outputFileWriter.write(stubbedSelection);

expect(writeFileSpy).toHaveBeenCalledTimes(2);
expect(writeFileInvocations).toEqual(expectations);
});
})
});
Loading