diff --git a/.github/workflows/E2E.yml b/.github/workflows/E2E.yml index 60d1e063..b367b92a 100644 --- a/.github/workflows/E2E.yml +++ b/.github/workflows/E2E.yml @@ -41,7 +41,7 @@ jobs: run: solhint --version - name: Run E2E Tests - run: cd e2e && npm install && npm test + run: cd e2e && npm install && npm test && npm test-formatter e2e_windows: runs-on: windows-latest @@ -81,7 +81,7 @@ jobs: run: npm run lint - name: Run E2E Tests - run: cd e2e && npm install && npm test + run: cd e2e && npm install && npm test && npm test-formatter e2e_macos: runs-on: macos-latest @@ -106,4 +106,4 @@ jobs: run: npm run lint - name: Run Tests - run: cd e2e && npm install && npm test + run: cd e2e && npm install && npm test && npm test-formatter diff --git a/README.md b/README.md index 6d8e70e3..0f010f69 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Linter for Solidity programming language Options: -V, --version output the version number - -f, --formatter [name] report formatter name (stylish, table, tap, unix, json) + -f, --formatter [name] report formatter name (stylish, table, tap, unix, json, compact) -w, --max-warnings [maxWarningsNumber] number of allowed warnings -c, --config [file_name] file to use as your .solhint.json -q, --quiet report errors only - default: false diff --git a/e2e/06-formatters/.solhint.json b/e2e/06-formatters/.solhint.json new file mode 100644 index 00000000..3b8ee84a --- /dev/null +++ b/e2e/06-formatters/.solhint.json @@ -0,0 +1,3 @@ +{ + "extends": "solhint:all" +} diff --git a/e2e/06-formatters/contracts/Foo.sol b/e2e/06-formatters/contracts/Foo.sol new file mode 100644 index 00000000..1151c60a --- /dev/null +++ b/e2e/06-formatters/contracts/Foo.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +contract Foo { + uint256 public constant test1 = 1; + uint256 TEST2; + + constructor() { + + } +} diff --git a/e2e/06-formatters/contracts/Foo2.sol b/e2e/06-formatters/contracts/Foo2.sol new file mode 100644 index 00000000..9fb98013 --- /dev/null +++ b/e2e/06-formatters/contracts/Foo2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.8; + +contract Foo { + uint256 public constant test1 = 1; + + constructor() { + + } +} diff --git a/e2e/06-formatters/contracts/Foo3.sol b/e2e/06-formatters/contracts/Foo3.sol new file mode 100644 index 00000000..f89c6912 --- /dev/null +++ b/e2e/06-formatters/contracts/Foo3.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.8; + +contract Foo { + uint256 public constant TEST1 = 1; + uint256 public value; + + function _goodContract() private { + value = TEST1; + } +} diff --git a/e2e/06-formatters/helpers/helpers.js b/e2e/06-formatters/helpers/helpers.js new file mode 100644 index 00000000..05089917 --- /dev/null +++ b/e2e/06-formatters/helpers/helpers.js @@ -0,0 +1,99 @@ +const foo1Output = [ + { + line: 2, + column: 1, + severity: 'Error', + message: 'Compiler version >=0.6.0 does not satisfy the ^0.5.8 semver requirement', + ruleId: 'compiler-version', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 5, + column: 5, + severity: 'Warning', + message: 'Constant name must be in capitalized SNAKE_CASE', + ruleId: 'const-name-snakecase', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 6, + column: 5, + severity: 'Warning', + message: 'Explicitly mark visibility of state', + ruleId: 'state-visibility', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 6, + column: 5, + severity: 'Warning', + message: "'TEST2' should start with _", + ruleId: 'private-vars-leading-underscore', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 6, + column: 5, + severity: 'Warning', + message: 'Variable name must be in mixedCase', + ruleId: 'var-name-mixedcase', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 8, + column: 5, + severity: 'Warning', + message: + 'Explicitly mark visibility in function (Set ignoreConstructors to true if using solidity >=0.7.0)', + ruleId: 'func-visibility', + fix: null, + filePath: 'contracts/Foo.sol', + }, + { + line: 8, + column: 19, + severity: 'Warning', + message: 'Code contains empty blocks', + ruleId: 'no-empty-blocks', + fix: null, + filePath: 'contracts/Foo.sol', + }, +] + +const foo2Output = [ + { + line: 5, + column: 5, + severity: 'Warning', + message: 'Constant name must be in capitalized SNAKE_CASE', + ruleId: 'const-name-snakecase', + fix: null, + filePath: 'contracts/Foo2.sol', + }, + { + line: 7, + column: 5, + severity: 'Warning', + message: + 'Explicitly mark visibility in function (Set ignoreConstructors to true if using solidity >=0.7.0)', + ruleId: 'func-visibility', + fix: null, + filePath: 'contracts/Foo2.sol', + }, + { + line: 7, + column: 19, + severity: 'Warning', + message: 'Code contains empty blocks', + ruleId: 'no-empty-blocks', + fix: null, + filePath: 'contracts/Foo2.sol', + }, +] + +module.exports = { foo1Output, foo2Output } diff --git a/e2e/formatters-test.js b/e2e/formatters-test.js new file mode 100644 index 00000000..341e5090 --- /dev/null +++ b/e2e/formatters-test.js @@ -0,0 +1,345 @@ +const chai = require('chai') +const { expect } = chai +const shell = require('shelljs') + +function useFixture(dir) { + beforeEach(`switch to ${dir}`, function () { + const fixturePath = path.join(__dirname, dir) + + const tmpDirContainer = os.tmpdir() + this.testDirPath = path.join(tmpDirContainer, `solhint-tests-${dir}`) + + fs.ensureDirSync(this.testDirPath) + fs.emptyDirSync(this.testDirPath) + + fs.copySync(fixturePath, this.testDirPath) + + shell.cd(this.testDirPath) + }) +} + +describe('e2e', function () { + describe('formatter tests', () => { + // Foo contract has 1 error and 6 warnings + // Foo2 contract has 3 warnings + // Foo3 contract has no warnings and no errors + const { foo1Output, foo2Output } = require('./06-formatters/helpers/helpers.js') + const PATH = '' + + useFixture('06-formatters') + + it('should fail when wrong formatter is specify', () => { + const formatterType = 'wrongOne' + const { code } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + expect(code).to.equal(1) + }) + + describe('unix formatter tests', () => { + const formatterType = 'unix' + + it('should return nothing when file does not exist and unix is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + + it('should return nothing when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + it('should make the output report with unix formatter for Foo2', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + + const reportLines = stdout.split('\n') + let expectedLine + + for (let i = 0; i < reportLines.length - 3; i++) { + expectedLine = `${foo2Output[i].filePath}:${foo2Output[i].line}:${foo2Output[i].column}: ${foo2Output[i].message} [${foo2Output[i].severity}/${foo2Output[i].ruleId}]` + expect(reportLines[i]).to.equal(expectedLine) + } + expect(code).to.equal(0) + + const finalLine = '3 problem/s (3 warning/s) ' + expect(reportLines[reportLines.length - 2]).to.equal(finalLine) + }) + it('should make the output report with unix formatter for Foo and Foo2 and Foo3', () => { + const { code, stdout } = shell.exec( + `solhint ${PATH}contracts/Foo.sol ${PATH}contracts/Foo2.sol ${PATH}contracts/Foo3.sol -f ${formatterType}` + ) + + const reportLines = stdout.split('\n') + const joinedFoo = foo1Output.concat(foo2Output) + let expectedLine + + for (let i = 0; i < reportLines.length - 3; i++) { + expectedLine = `${joinedFoo[i].filePath}:${joinedFoo[i].line}:${joinedFoo[i].column}: ${joinedFoo[i].message} [${joinedFoo[i].severity}/${joinedFoo[i].ruleId}]` + expect(reportLines[i]).to.equal(expectedLine) + } + // because there's an error + expect(code).to.equal(1) + + const finalLine = '10 problem/s (1 error/s, 9 warning/s)' + expect(reportLines[reportLines.length - 2]).to.contain(finalLine) + }) + }) + + describe('json formatter tests', () => { + const formatterType = 'json' + + it('should return nothing when file does not exist and json is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + it('should return nothing when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + it('should make the output report with json formatter for Foo2', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + + const expectedFinalOutput = foo2Output.concat([{ conclusion: '3 problem/s (3 warning/s)' }]) + + // eslint-disable-next-line no-eval + const objectOutput = eval('(' + stdout + ')') + const strOutput = JSON.stringify(objectOutput) + const strExpected = JSON.stringify(expectedFinalOutput) + expect(strExpected).to.equal(strOutput) + expect(code).to.equal(0) + }) + it('should make the output report with unix formatter for Foo and Foo2 and Foo3', () => { + const { code, stdout } = shell.exec( + `solhint ${PATH}contracts/Foo.sol ${PATH}contracts/Foo2.sol ${PATH}contracts/Foo3.sol -f ${formatterType}` + ) + + const expectedFinalOutput = foo1Output + .concat(foo2Output) + .concat([{ conclusion: '10 problem/s (1 error/s, 9 warning/s)' }]) + + // eslint-disable-next-line no-eval + const objectOutput = eval('(' + stdout + ')') + const strOutput = JSON.stringify(objectOutput) + const strExpected = JSON.stringify(expectedFinalOutput) + expect(strExpected).to.equal(strOutput) + // There's an error, that is why exit code is 1 + expect(code).to.equal(1) + }) + }) + + describe('compact formatter tests', () => { + const formatterType = 'compact' + + it('should return nothing when file does not exist and compact is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + + it('should return nothing when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + it('should make the output report with compact formatter for Foo2', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + + const reportLines = stdout.split('\n') + let expectedLine + + for (let i = 0; i < reportLines.length - 3; i++) { + expectedLine = `${foo2Output[i].filePath}: line ${foo2Output[i].line}, col ${foo2Output[i].column}, ${foo2Output[i].severity} - ${foo2Output[i].message} (${foo2Output[i].ruleId})` + expect(reportLines[i]).to.equal(expectedLine) + } + expect(code).to.equal(0) + + const finalLine = '3 problem/s (3 warning/s) ' + expect(reportLines[reportLines.length - 2]).to.equal(finalLine) + }) + it('should make the output report with compact formatter for Foo and Foo2 and Foo3', () => { + const { code, stdout } = shell.exec( + `solhint ${PATH}contracts/Foo.sol ${PATH}contracts/Foo2.sol ${PATH}contracts/Foo3.sol -f ${formatterType}` + ) + + const reportLines = stdout.split('\n') + const joinedFoo = foo1Output.concat(foo2Output) + let expectedLine + + for (let i = 0; i < reportLines.length - 3; i++) { + expectedLine = `${joinedFoo[i].filePath}: line ${joinedFoo[i].line}, col ${joinedFoo[i].column}, ${joinedFoo[i].severity} - ${joinedFoo[i].message} (${joinedFoo[i].ruleId})` + expect(reportLines[i]).to.equal(expectedLine) + } + // because there's an error + expect(code).to.equal(1) + + const finalLine = '10 problem/s (1 error/s, 9 warning/s)' + expect(reportLines[reportLines.length - 2]).to.contain(finalLine) + }) + }) + + describe('stylish formatter tests', () => { + const formatterType = 'stylish' + + it('should return nothing when file does not exist and stylish is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + + it('should return nothing when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + expect(code).to.equal(0) + expect(stdout.trim()).to.be.empty + }) + it('should make the output report with stylish formatter for Foo2', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + + const reportLines = stdout.split('\n') + let expectedLine = foo2Output[0].filePath + expect(reportLines[1]).to.equal(expectedLine) + + for (let i = 2; i < reportLines.length - 4; i++) { + expectedLine = ` ${foo2Output[i - 2].line}:${foo2Output[i - 2].column} ${foo2Output[ + i - 2 + ].severity.toLowerCase()} ${foo2Output[i - 2].message} ${foo2Output[i - 2].ruleId}` + + expect(reportLines[i].replace(/\s/g, '')).to.equal(expectedLine.replace(/\s/g, '')) + } + expect(code).to.equal(0) + + const finalLine = '\u2716 3 problems (0 errors, 3 warnings)' + expect(reportLines[reportLines.length - 3]).to.equal(finalLine) + }) + it('should make the output report with stylish formatter for Foo and Foo2 and Foo3', () => { + const { code, stdout } = shell.exec( + `solhint ${PATH}contracts/Foo.sol ${PATH}contracts/Foo2.sol ${PATH}contracts/Foo3.sol -f ${formatterType}` + ) + + const reportLines = stdout.split('\n') + + let expectedLine + expectedLine = foo1Output[0].filePath + expect(reportLines[1]).to.equal(expectedLine) + + expectedLine = foo2Output[0].filePath + expect(reportLines[10]).to.equal(expectedLine) + + const joinedFoo = foo1Output.concat(foo2Output) + reportLines.splice(9, 2) + reportLines.splice(1, 1) + + for (let i = 1; i < reportLines.length - 4; i++) { + expectedLine = ` ${joinedFoo[i - 1].line}:${joinedFoo[i - 1].column} ${joinedFoo[ + i - 1 + ].severity.toLowerCase()} ${joinedFoo[i - 1].message} ${joinedFoo[i - 1].ruleId}` + + expect(reportLines[i].replace(/\s/g, '')).to.equal(expectedLine.replace(/\s/g, '')) + } + expect(code).to.equal(1) + + const finalLine = '\u2716 10 problems (1 error, 9 warnings)' + expect(reportLines[reportLines.length - 3]).to.equal(finalLine) + }) + }) + + describe('tap formatter tests', () => { + const formatterType = 'tap' + + it('should return TAP header when file does not exist and tap is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + + const reportLines = stdout.split('\n') + expect(reportLines[0]).to.eq('TAP version 13') + expect(reportLines[1]).to.eq('1..0') + + expect(code).to.equal(0) + }) + + it('should return TAP header [ok 1] when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + const reportLines = stdout.split('\n') + expect(reportLines[0]).to.eq('TAP version 13') + expect(reportLines[1]).to.eq('1..1') + expect(reportLines[2]).to.eq(`ok 1 - ${PATH}contracts/Foo3.sol`) + + expect(code).to.equal(0) + }) + it('should make the output report with tap formatter for Foo2', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo2.sol -f ${formatterType}`) + + const reportLines = stdout.split('\n') + expect(reportLines[0]).to.eq('TAP version 13') + expect(reportLines[1]).to.eq('1..1') + expect(reportLines[2]).to.eq(`ok 1 - ${foo2Output[0].filePath}`) + expect(reportLines[4]).to.eq(` message: ${foo2Output[0].message}`) + expect(reportLines[5]).to.eq(` severity: ${foo2Output[0].severity.toLowerCase()}`) + expect(reportLines[6]).to.eq(` data:`) + expect(reportLines[7]).to.eq(` line: ${foo2Output[0].line}`) + expect(reportLines[8]).to.eq(` column: ${foo2Output[0].column}`) + expect(reportLines[9]).to.eq(` ruleId: ${foo2Output[0].ruleId}`) + + expect(code).to.equal(0) + }) + }) + + describe('table formatter tests', () => { + const formatterType = 'table' + const tableFooter1 = + '╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗' + let tableFooter2 = `║ 0 Errors ║` + const tableFooter3 = + '╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢' + let tableFooter4 = `║ 0 Warnings ║` + const tableFooter5 = + '╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝' + const tableHeader1 = + '║ Line │ Column │ Type │ Message │ Rule ID ║' + const tableHeader2 = + '╟──────────┼──────────┼──────────┼────────────────────────────────────────────────────────┼──────────────────────╢' + + it('should return TABLE Footer when file does not exist and table is the formatter', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo1.sol -f ${formatterType}`) + const reportLines = stdout.split('\n') + expect(reportLines[1]).to.eq(tableFooter1) + expect(reportLines[2]).to.eq(tableFooter2) + expect(reportLines[3]).to.eq(tableFooter3) + expect(reportLines[4]).to.eq(tableFooter4) + expect(reportLines[5]).to.eq(tableFooter5) + + expect(code).to.equal(0) + }) + + it('should return TABLE Footer when file exists and there is no error/warning', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo3.sol -f ${formatterType}`) + const reportLines = stdout.split('\n') + expect(reportLines[1]).to.eq(tableFooter1) + expect(reportLines[2]).to.eq(tableFooter2) + expect(reportLines[3]).to.eq(tableFooter3) + expect(reportLines[4]).to.eq(tableFooter4) + expect(reportLines[5]).to.eq(tableFooter5) + + expect(code).to.equal(0) + }) + it('should make the output report with table formatter for Foo', () => { + const { code, stdout } = shell.exec(`solhint ${PATH}contracts/Foo.sol -f ${formatterType}`) + const reportLines = stdout.split('\n') + + expect(reportLines[1]).to.eq(foo1Output[0].filePath) + + tableFooter2 = tableFooter2.replace('0 Errors', '1 Error ') + tableFooter4 = tableFooter4.replace('0', '6') + expect(reportLines[17]).to.eq(tableFooter1) + expect(reportLines[18]).to.eq(tableFooter2) + expect(reportLines[19]).to.eq(tableFooter3) + expect(reportLines[20]).to.eq(tableFooter4) + expect(reportLines[21]).to.eq(tableFooter5) + + expect(reportLines[3]).to.eq(tableHeader1) + expect(reportLines[4]).to.eq(tableHeader2) + + expect(code).to.equal(1) + }) + }) + }) +}) diff --git a/e2e/package.json b/e2e/package.json index 8222399a..428c4e87 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -4,12 +4,14 @@ "description": "E2E tests for solhint", "main": "index.js", "scripts": { - "test": "mocha test.js" + "test": "mocha test.js", + "test-formatter": "mocha formatters-test.js" }, "author": "", "license": "MIT", "devDependencies": { "chai": "^4.3.7", + "chai-spies": "^1.0.0", "fs-extra": "^11.1.0", "get-stream": "^6.0.0", "mocha": "^10.2.0", diff --git a/e2e/test.js b/e2e/test.js index a38383c4..14f024fa 100644 --- a/e2e/test.js +++ b/e2e/test.js @@ -1,4 +1,5 @@ -const { expect } = require('chai') +const chai = require('chai') +const { expect } = chai const cp = require('child_process') const fs = require('fs-extra') const getStream = require('get-stream') @@ -113,7 +114,7 @@ describe('e2e', function () { }) }) - describe.only('--max-warnings parameter tests', function () { + describe('--max-warnings parameter tests', function () { // Foo contract has 6 warnings // Foo2 contract has 1 error and 14 warnings useFixture('05-max-warnings') @@ -128,13 +129,13 @@ describe('e2e', function () { }) it('should display [warnings exceeded] for max 3 warnings and exit error 1', function () { - const { code, stdout, } = shell.exec('solhint contracts/Foo.sol --max-warnings 3') + const { code, stdout } = shell.exec('solhint contracts/Foo.sol --max-warnings 3') expect(code).to.equal(1) expect(stdout.trim()).to.contain(warningExceededMsg) }) it('should return error for Compiler version rule, ignoring 3 --max-warnings', function () { - const { code, stdout } = shell.exec('solhint contracts/Foo2.sol --max-warnings 3') + const { code, stdout } = shell.exec('solhint contracts/Foo2.sol --max-warnings 3') expect(code).to.equal(1) expect(stdout.trim()).to.contain(errorFound) }) diff --git a/lib/formatters/README.md b/lib/formatters/README.md index 5c0b4152..9d6ee6c1 100644 --- a/lib/formatters/README.md +++ b/lib/formatters/README.md @@ -2,8 +2,10 @@ files in this directory are pulled from eslint repository: -- table.js: eslint v5.6.0 Gajus Kuizinas -- unix.js: eslint v8.32.0 oshi-shinobu -- tap.js: eslint v8.32.0 Jonathan Kingston -- stylish.js: eslint v8.32.0 by Sindre Sorhus -- json.js: eslint v8.32.0 by Artur Lukianov & Diego Bale +- table.js: eslint - Gajus Kuizinas +- unix.js: eslint - oshi-shinobu +- tap.js: eslint - Jonathan Kingston +- stylish.js: eslint - Sindre Sorhus +- json.js: eslint - Artur Lukianov +- compact.js: eslint - Nicholas C. Zakas + \ No newline at end of file diff --git a/lib/formatters/compact.js b/lib/formatters/compact.js new file mode 100644 index 00000000..56ebebed --- /dev/null +++ b/lib/formatters/compact.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Compact reporter + * @author Nicholas C. Zakas + */ + +//------------------------------------------------------------------------------ +// Helper Functions +//------------------------------------------------------------------------------ + +/** + * Returns the severity of warning or error + * @param {Object} message message object to examine + * @returns {string} severity level + * @private + */ +function getMessageType(message) { + if (message.fatal || message.severity === 2) { + return 'Error' + } + return 'Warning' +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +// eslint-disable-next-line func-names +module.exports = function (results) { + let output = '' + let total = 0 + let errors = 0 + let warnings = 0 + + results.forEach((result) => { + const messages = result.messages + + total += messages.length + + messages.forEach((message) => { + output += `${result.filePath}: ` + output += `line ${message.line || 0}` + output += `, col ${message.column || 0}` + output += `, ${getMessageType(message)}` + output += ` - ${message.message}` + output += message.ruleId ? ` (${message.ruleId})` : '' + output += '\n' + if (message.severity === 2) errors += 1 + else warnings += 1 + }) + }) + + let finalMessage = '' + if (errors > 0 && warnings > 0) { + finalMessage = `${errors + warnings} problem/s (${errors} error/s, ${warnings} warning/s)` + } else if (errors > 0 && warnings === 0) { + finalMessage = `${errors} problem/s (${errors} error/s)` + } else if (errors === 0 && warnings > 0) { + finalMessage = `${warnings} problem/s (${warnings} warning/s)` + } + + if (total > 0) { + output += `\n${finalMessage}` + } + + return output +} diff --git a/lib/formatters/json.js b/lib/formatters/json.js index d46c7ed8..2bf6f8bf 100644 --- a/lib/formatters/json.js +++ b/lib/formatters/json.js @@ -1,7 +1,6 @@ /** * @fileoverview JSON Style formatter * @author ArturLukianov - * @collaborator Diego Bale */ //------------------------------------------------------------------------------ @@ -27,6 +26,8 @@ function getMessageType(message) { // eslint-disable-next-line func-names module.exports = function (results) { const allMessages = [] + let errors = 0 + let warnings = 0 results.forEach((result) => { const messages = result.messages @@ -34,9 +35,28 @@ module.exports = function (results) { messages.forEach((message) => { const fullObject = { ...message, filePath: result.filePath } fullObject.severity = getMessageType(fullObject) + if (fullObject.severity === 'Error') errors += 1 + else warnings += 1 allMessages.push(fullObject) }) }) - return JSON.parse(JSON.stringify(allMessages)) + let finalMessage + if (errors > 0 && warnings > 0) { + finalMessage = { + conclusion: `${errors + warnings} problem/s (${errors} error/s, ${warnings} warning/s)`, + } + } else if (errors > 0 && warnings === 0) { + finalMessage = { + conclusion: `${errors} problem/s (${errors} error/s)`, + } + } else if (errors === 0 && warnings > 0) { + finalMessage = { + conclusion: `${warnings} problem/s (${warnings} warning/s)`, + } + } + + if (finalMessage) allMessages.push(finalMessage) + + return allMessages.length > 0 ? JSON.parse(JSON.stringify(allMessages)) : '' } diff --git a/lib/formatters/unix.js b/lib/formatters/unix.js index 0029088e..581f7588 100644 --- a/lib/formatters/unix.js +++ b/lib/formatters/unix.js @@ -27,6 +27,8 @@ function getMessageType(message) { module.exports = function (results) { let output = '' let total = 0 + let errors = 0 + let warnings = 0 results.forEach((result) => { const messages = result.messages @@ -40,11 +42,22 @@ module.exports = function (results) { output += ` ${message.message} ` output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ''}]` output += '\n' + if (message.severity === 2) errors += 1 + else warnings += 1 }) }) + let finalMessage = '' + if (errors > 0 && warnings > 0) { + finalMessage = `${errors + warnings} problem/s (${errors} error/s, ${warnings} warning/s)` + } else if (errors > 0 && warnings === 0) { + finalMessage = `${errors} problem/s (${errors} error/s)` + } else if (errors === 0 && warnings > 0) { + finalMessage = `${warnings} problem/s (${warnings} warning/s)` + } + if (total > 0) { - output += `\n${total} problem${total !== 1 ? 's' : ''}` + output += `\n${finalMessage}` } return output diff --git a/solhint.js b/solhint.js index 6d6cf188..f1d64006 100644 --- a/solhint.js +++ b/solhint.js @@ -19,7 +19,10 @@ function init() { program .name('solhint') .usage('[options] [...other_files]') - .option('-f, --formatter [name]', 'report formatter name (stylish, table, tap, unix, json)') + .option( + '-f, --formatter [name]', + 'report formatter name (stylish, table, tap, unix, json, compact)' + ) .option('-w, --max-warnings [maxWarningsNumber]', 'number of allowed warnings') .option('-c, --config [file_name]', 'file to use as your .solhint.json') .option('-q, --quiet', 'report errors only - default: false')