diff --git a/.editorconfig b/.editorconfig index 70a03f65a..6f2e8d53f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,10 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.ts] +indent_style = tab +indent_size = 4 + [*.java] indent_style = space indent_size = 4 diff --git a/messages/rules-command.md b/messages/rules-command.md index 71eac2183..584f2486e 100644 --- a/messages/rules-command.md +++ b/messages/rules-command.md @@ -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. diff --git a/messages/rules-writer.md b/messages/rules-writer.md index 1959d8519..dcdc94638 100644 --- a/messages/rules-writer.md +++ b/messages/rules-writer.md @@ -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 diff --git a/package-lock.json b/package-lock.json index f81ecb84a..d7c2689b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD-3-Clause", "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", @@ -4663,9 +4663,9 @@ } }, "node_modules/@salesforce/code-analyzer-core": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-core/-/code-analyzer-core-0.32.0.tgz", - "integrity": "sha512-4s1alOge4df/G+DB/vBSv7jO8OEiDmLVXkEwnYZVy9djU543b7T7zDY4b8moqZ51FizjCXEOd4ejr/IH+8/wJg==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@salesforce/code-analyzer-core/-/code-analyzer-core-0.33.0.tgz", + "integrity": "sha512-e9m7UQFbxXrjBncTrQHkvKZVbGaYH1d5GFaT2mEIil2kTcoYNRNnKGB9JP+E9V9AioJR+8TT+y2HX1I6TXCnMg==", "license": "BSD-3-Clause", "dependencies": { "@salesforce/code-analyzer-engine-api": "0.27.0", diff --git a/package.json b/package.json index 211c86af0..35dda21bb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/commands/code-analyzer/rules.ts b/src/commands/code-analyzer/rules.ts index 8687abcad..b3c37f515 100644 --- a/src/commands/code-analyzer/rules.ts +++ b/src/commands/code-analyzer/rules.ts @@ -48,10 +48,12 @@ export default class RulesCommand extends SfCommand 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'), @@ -63,7 +65,7 @@ export default class RulesCommand extends SfCommand implements Displayable public async run(): Promise { 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); diff --git a/src/lib/writers/RulesWriter.ts b/src/lib/writers/RulesWriter.ts index 75047592d..13c150ce8 100644 --- a/src/lib/writers/RulesWriter.ts +++ b/src/lib/writers/RulesWriter.ts @@ -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); diff --git a/test/commands/code-analyzer/rules.test.ts b/test/commands/code-analyzer/rules.test.ts index 95ebc5d5d..45f4844ae 100644 --- a/test/commands/code-analyzer/rules.test.ts +++ b/test/commands/code-analyzer/rules.test.ts @@ -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(); @@ -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 () => { diff --git a/test/lib/writers/RulesWriter.test.ts b/test/lib/writers/RulesWriter.test.ts index 392195ae7..5a8abdeea 100644 --- a/test/lib/writers/RulesWriter.test.ts +++ b/test/lib/writers/RulesWriter.test.ts @@ -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]); + }); + }); - 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); - }); - }) -}); \ No newline at end of file + 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); + }); + }) +});