├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md ├── screenshot.png └── test ├── fixtures ├── data.json ├── default.json ├── line-numbers.json ├── messages.json ├── no-line-numbers.json └── sort-by-severity-then-line-then-column.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {ESLint, Linter} from 'eslint'; 2 | 3 | /** 4 | Pretty formatter for [ESLint](https://eslint.org). 5 | 6 | @param results - Lint result for the individual files. 7 | @param data - Extended information related to the analysis results. 8 | @returns The formatted output. 9 | */ 10 | export default function eslintFormatterPretty( 11 | results: LintResult[], 12 | data?: LintResultData 13 | ): string; 14 | 15 | export type LintResult = ESLint.LintResult; 16 | export type LintResultData = ESLint.LintResultData; 17 | export type Severity = Linter.Severity; 18 | export type LintMessage = Linter.LintMessage; 19 | 20 | export {Linter} from 'eslint'; 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import path from 'node:path'; 3 | import chalk from 'chalk'; 4 | import logSymbols from 'log-symbols'; 5 | import plur from 'plur'; 6 | import stringWidth from 'string-width'; 7 | import ansiEscapes from 'ansi-escapes'; 8 | import {supportsHyperlink} from 'supports-hyperlinks'; 9 | import getRuleDocs from 'eslint-rule-docs'; 10 | 11 | export default function eslintFormatterPretty(results, data) { 12 | const lines = []; 13 | let errorCount = 0; 14 | let warningCount = 0; 15 | let maxLineWidth = 0; 16 | let maxColumnWidth = 0; 17 | let maxMessageWidth = 0; 18 | let showLineNumbers = false; 19 | 20 | for (const result of results 21 | .sort((a, b) => { 22 | if (a.errorCount === b.errorCount) { 23 | return b.warningCount - a.warningCount; 24 | } 25 | 26 | if (a.errorCount === 0) { 27 | return -1; 28 | } 29 | 30 | if (b.errorCount === 0) { 31 | return 1; 32 | } 33 | 34 | return b.errorCount - a.errorCount; 35 | })) { 36 | const {messages, filePath} = result; 37 | 38 | if (messages.length === 0) { 39 | continue; 40 | } 41 | 42 | errorCount += result.errorCount; 43 | warningCount += result.warningCount; 44 | 45 | if (lines.length > 0) { 46 | lines.push({type: 'separator'}); 47 | } 48 | 49 | const firstErrorOrWarning = messages.find(({severity}) => severity === 2) ?? messages[0]; 50 | 51 | lines.push({ 52 | type: 'header', 53 | filePath, 54 | relativeFilePath: path.relative('.', filePath), 55 | firstLineCol: firstErrorOrWarning.line + ':' + firstErrorOrWarning.column, 56 | }); 57 | 58 | for (const x of messages 59 | .sort((a, b) => { 60 | if (a.fatal === b.fatal && a.severity === b.severity) { 61 | if (a.line === b.line) { 62 | return a.column < b.column ? -1 : 1; 63 | } 64 | 65 | return a.line < b.line ? -1 : 1; 66 | } 67 | 68 | if ((a.fatal || a.severity === 2) && (!b.fatal || b.severity !== 2)) { 69 | return 1; 70 | } 71 | 72 | return -1; 73 | })) { 74 | let {message} = x; 75 | 76 | // Stylize inline code blocks 77 | message = message.replaceAll(/\B`(.*?)`\B|\B'(.*?)'\B/g, (m, p1, p2) => chalk.bold(p1 ?? p2)); 78 | 79 | const line = String(x.line ?? 0); 80 | const column = String(x.column ?? 0); 81 | const lineWidth = stringWidth(line); 82 | const columnWidth = stringWidth(column); 83 | const messageWidth = stringWidth(message); 84 | 85 | maxLineWidth = Math.max(lineWidth, maxLineWidth); 86 | maxColumnWidth = Math.max(columnWidth, maxColumnWidth); 87 | maxMessageWidth = Math.max(messageWidth, maxMessageWidth); 88 | showLineNumbers = showLineNumbers || x.line || x.column; 89 | 90 | lines.push({ 91 | type: 'message', 92 | severity: (x.fatal || x.severity === 2 || x.severity === 'error') ? 'error' : 'warning', 93 | line, 94 | lineWidth, 95 | column, 96 | columnWidth, 97 | message, 98 | messageWidth, 99 | ruleId: x.ruleId ?? '', 100 | }); 101 | } 102 | } 103 | 104 | let output = '\n'; 105 | 106 | if (process.stdout.isTTY && !process.env.CI && process.env.TERM_PROGRAM === 'iTerm.app') { 107 | // Make relative paths Command-clickable in iTerm 108 | output += ansiEscapes.iTerm.setCwd(); 109 | } 110 | 111 | output += lines.map(x => { 112 | if (x.type === 'header') { 113 | // Add the line number so it's Command-click'able in some terminals 114 | // Use dim & gray for terminals like iTerm that doesn't support `hidden` 115 | const position = showLineNumbers ? chalk.hidden.dim.gray(`:${x.firstLineCol}`) : ''; 116 | 117 | return ' ' + chalk.underline(x.relativeFilePath) + position; 118 | } 119 | 120 | if (x.type === 'message') { 121 | let ruleUrl; 122 | 123 | try { 124 | ruleUrl = data.rulesMeta[x.ruleId].docs.url; 125 | } catch { 126 | try { 127 | ruleUrl = getRuleDocs(x.ruleId).url; 128 | } catch {} 129 | } 130 | 131 | const line = [ 132 | '', 133 | x.severity === 'warning' ? logSymbols.warning : logSymbols.error, 134 | ' '.repeat(maxLineWidth - x.lineWidth) + chalk.dim(x.line + chalk.gray(':') + x.column), 135 | ' '.repeat(maxColumnWidth - x.columnWidth) + x.message, 136 | ' '.repeat(maxMessageWidth - x.messageWidth) 137 | + (ruleUrl && supportsHyperlink(process.stdout) ? ansiEscapes.link(chalk.dim(x.ruleId), ruleUrl) : chalk.dim(x.ruleId)), 138 | ]; 139 | 140 | if (!showLineNumbers) { 141 | line.splice(2, 1); 142 | } 143 | 144 | return line.join(' '); 145 | } 146 | 147 | return ''; 148 | }).join('\n') + '\n\n'; 149 | 150 | if (warningCount > 0) { 151 | output += ' ' + chalk.yellow(`${warningCount} ${plur('warning', warningCount)}`) + '\n'; 152 | } 153 | 154 | if (errorCount > 0) { 155 | output += ' ' + chalk.red(`${errorCount} ${plur('error', errorCount)}`) + '\n'; 156 | } 157 | 158 | return (errorCount + warningCount) > 0 ? output : ''; 159 | } 160 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import type {ESLint} from 'eslint'; 2 | import {expectType} from 'tsd'; 3 | import eslintFormatterPretty, { 4 | type LintResult, 5 | type LintMessage, 6 | type Severity, 7 | } from './index.js'; 8 | 9 | // Test LintResult interface members 10 | declare const lintResult: LintResult; 11 | expectType(lintResult.filePath); 12 | expectType(lintResult.errorCount); 13 | expectType(lintResult.warningCount); 14 | expectType(lintResult.messages); 15 | 16 | // Test LintMessage interface members 17 | const lintMessage = lintResult.messages[0]; 18 | expectType(lintMessage.severity); 19 | expectType(lintMessage.message); 20 | expectType(lintMessage.fatal); 21 | expectType(lintMessage.line); 22 | expectType(lintMessage.column); 23 | expectType(lintMessage.ruleId); // eslint-disable-line @typescript-eslint/ban-types 24 | 25 | // Test formatterPretty() 26 | declare const lintResults: LintResult[]; 27 | declare const eslintLintResults: ESLint.LintResult[]; 28 | declare const lintResultData: ESLint.LintResultData; 29 | 30 | expectType(eslintFormatterPretty(lintResults)); 31 | expectType(eslintFormatterPretty(eslintLintResults)); 32 | expectType(eslintFormatterPretty(eslintLintResults, lintResultData)); 33 | 34 | // FIXME 35 | // type PartialLintResult = { 36 | // filePath: string; 37 | // errorCount: number; 38 | // warningCount: number; 39 | // messages: Array<{ 40 | // fileName: string; 41 | // message: string; 42 | // severity: 0 | 1 | 2; 43 | // line?: number; 44 | // column?: number; 45 | // }>; 46 | // }; 47 | 48 | // declare const partialLintResults: PartialLintResult[]; 49 | 50 | // expectType(eslintFormatterPretty(partialLintResults)); 51 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-formatter-pretty", 3 | "version": "6.0.1", 4 | "description": "Pretty ESLint formatter", 5 | "license": "MIT", 6 | "repository": "sindresorhus/eslint-formatter-pretty", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "eslint", 31 | "eslint-formatter", 32 | "formatter", 33 | "reporter", 34 | "lint", 35 | "validate" 36 | ], 37 | "dependencies": { 38 | "@types/eslint": "^8.44.6", 39 | "ansi-escapes": "^6.2.0", 40 | "chalk": "^5.3.0", 41 | "eslint-rule-docs": "^1.1.235", 42 | "log-symbols": "^6.0.0", 43 | "plur": "^5.1.0", 44 | "string-width": "^7.0.0", 45 | "supports-hyperlinks": "^3.0.0" 46 | }, 47 | "devDependencies": { 48 | "ava": "^5.3.1", 49 | "strip-ansi": "^7.1.0", 50 | "tsd": "^0.29.0", 51 | "typescript": "^5.2.2", 52 | "xo": "^0.56.0" 53 | }, 54 | "ava": { 55 | "serial": true 56 | }, 57 | "xo": { 58 | "rules": { 59 | "import/no-extraneous-dependencies": "off" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # eslint-formatter-pretty 2 | 3 | > Pretty formatter for [ESLint](https://eslint.org) 4 | 5 | ![](screenshot.png) 6 | 7 | ## Highlights 8 | 9 | - Pretty output. 10 | - Sorts results by severity. 11 | - Stylizes inline codeblocks in messages. 12 | - Command-click a rule ID to open its docs. 13 | - Command-click a header to reveal the first error in your editor. *(iTerm-only)* 14 | 15 | ## Install 16 | 17 | ```sh 18 | npm install --save-dev eslint-formatter-pretty 19 | ``` 20 | 21 | *Please note that to use version 6 of this package you will HAVE to use ESLint v9+. If you're using ESLint v8 or below, install with `npm install --save-dev eslint-formatter-pretty@5` instead.* 22 | 23 | ## Usage 24 | 25 | ### [XO](https://github.com/xojs/xo) 26 | 27 | Nothing to do. It's the default formatter. 28 | 29 | ### ESLint CLI 30 | 31 | ```sh 32 | eslint --format=pretty file.js 33 | ``` 34 | 35 | ### [grunt-eslint](https://github.com/sindresorhus/grunt-eslint) 36 | 37 | ```js 38 | grunt.initConfig({ 39 | eslint: { 40 | target: ['file.js']. 41 | options: { 42 | format: 'pretty' 43 | } 44 | } 45 | }); 46 | 47 | grunt.loadNpmTasks('grunt-eslint'); 48 | grunt.registerTask('default', ['eslint']); 49 | ``` 50 | 51 | ### [gulp-eslint](https://github.com/adametry/gulp-eslint) 52 | 53 | ```js 54 | import gulp from 'gulp'; 55 | import eslint from 'gulp-eslint'; 56 | 57 | export const lint = ( 58 | gulp.src('file.js') 59 | .pipe(eslint()) 60 | .pipe(eslint.format('pretty')) 61 | ); 62 | ``` 63 | 64 | ### [eslint-loader](https://github.com/MoOx/eslint-loader) *(webpack)* 65 | 66 | ```js 67 | import eslintFormatterPretty from 'eslint-formatter-pretty'; 68 | 69 | export default { 70 | entry: ['file.js'], 71 | module: { 72 | rules: [ 73 | { 74 | test: /\.js$/, 75 | exclude: /node_modules/, 76 | loader: 'eslint-loader', 77 | options: { 78 | formatter: eslintFormatterPretty 79 | } 80 | } 81 | ] 82 | } 83 | }; 84 | ``` 85 | 86 | ## Tips 87 | 88 | In iTerm, Command-click the filename header to open the file in your editor. 89 | 90 | In [terminals with support for hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#supporting-apps), Command-click the rule ID to open its docs. 91 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/eslint-formatter-pretty/88f108852d01db020a4dbd65a56d05468195dcbe/screenshot.png -------------------------------------------------------------------------------- /test/fixtures/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesMeta": { 3 | "no-warning-comments": { 4 | "type": "suggestion", 5 | "docs": { 6 | "description": "disallow specified warning terms in comments", 7 | "category": "Best Practices", 8 | "recommended": false, 9 | "url": "https://eslint.org/docs/rules/test/no-warning-comments" 10 | }, 11 | "schema": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/index.js", 4 | "messages": [ 5 | { 6 | "ruleId": "no-warning-comments", 7 | "severity": 1, 8 | "message": "Unexpected 'todo' comment.", 9 | "line": 8, 10 | "column": 2, 11 | "nodeType": "Line", 12 | "source": "\t// TODO: fix this later" 13 | }, 14 | { 15 | "ruleId": "no-multiple-empty-lines", 16 | "severity": 2, 17 | "message": "More than 1 blank line not allowed.", 18 | "line": 18, 19 | "column": 2, 20 | "nodeType": "Program", 21 | "source": "" 22 | } 23 | ], 24 | "errorCount": 1, 25 | "warningCount": 1 26 | }, 27 | { 28 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/test.js", 29 | "messages": [ 30 | { 31 | "ruleId": "ava/use-test", 32 | "severity": 2, 33 | "message": "AVA should be imported as `test`.", 34 | "line": 1, 35 | "column": 1, 36 | "nodeType": "ImportDeclaration", 37 | "source": "import ava from 'ava';" 38 | } 39 | ], 40 | "errorCount": 1, 41 | "warningCount": 0 42 | }, 43 | { 44 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/foo.js", 45 | "messages": [ 46 | { 47 | "ruleId": "no-warning-comments", 48 | "severity": 1, 49 | "message": "Unexpected 'todo' comment.", 50 | "line": 8, 51 | "column": 2, 52 | "nodeType": "Line", 53 | "source": "\t// TODO: fix this later" 54 | } 55 | ], 56 | "errorCount": 0, 57 | "warningCount": 1 58 | }, 59 | { 60 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/foo.js", 61 | "messages": [ 62 | { 63 | "ruleId": "@typescript-eslint/no-unused-vars", 64 | "severity": 1, 65 | "message": "Unexpected 'todo' comment.", 66 | "line": 8, 67 | "column": 2, 68 | "nodeType": "Line", 69 | "source": "\t// TODO: fix this later" 70 | } 71 | ], 72 | "errorCount": 0, 73 | "warningCount": 1 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /test/fixtures/line-numbers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/index.js", 4 | "messages": [ 5 | { 6 | "ruleId": "no-warning-comments", 7 | "severity": 1, 8 | "message": "Unexpected 'todo' comment.", 9 | "nodeType": "Line", 10 | "source": "\t// TODO: fix this later" 11 | }, 12 | { 13 | "ruleId": "no-multiple-empty-lines", 14 | "severity": 2, 15 | "message": "More than 1 blank line not allowed.", 16 | "line": 18, 17 | "column": 2, 18 | "nodeType": "Program", 19 | "source": "" 20 | } 21 | ], 22 | "errorCount": 1, 23 | "warningCount": 1 24 | }, 25 | { 26 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/test.js", 27 | "messages": [ 28 | { 29 | "ruleId": "ava/use-test", 30 | "severity": 2, 31 | "message": "AVA should be imported as `test`.", 32 | "line": 1, 33 | "column": 1, 34 | "nodeType": "ImportDeclaration", 35 | "source": "import ava from 'ava';" 36 | } 37 | ], 38 | "errorCount": 1, 39 | "warningCount": 0 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /test/fixtures/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleId": "no-warning-comments", 4 | "severity": 1, 5 | "message": "Unexpected 'todo' comment.", 6 | "line": 1, 7 | "column": 1, 8 | "nodeType": "Line", 9 | "source": "\t// TODO: fix this later" 10 | }, 11 | { 12 | "ruleId": "no-multiple-empty-lines", 13 | "severity": 2, 14 | "message": "More than 1 blank line not allowed.", 15 | "line": 3, 16 | "column": 1, 17 | "nodeType": "Program", 18 | "source": "" 19 | }, 20 | { 21 | "ruleId": "no-warning-comments", 22 | "severity": 1, 23 | "message": "Unexpected 'todo' comment.", 24 | "line": 10, 25 | "column": 2, 26 | "nodeType": "Line", 27 | "source": "\t// TODO: fix this later" 28 | }, 29 | { 30 | "ruleId": "no-warning-comments", 31 | "severity": 1, 32 | "message": "Unexpected 'todo' comment.", 33 | "line": 15, 34 | "column": 2, 35 | "nodeType": "Line", 36 | "source": "\t// TODO: fix this later" 37 | }, 38 | { 39 | "ruleId": "no-multiple-empty-lines", 40 | "severity": 2, 41 | "message": "More than 1 blank line not allowed.", 42 | "line": 30, 43 | "column": 1, 44 | "nodeType": "Program", 45 | "source": "" 46 | }, 47 | { 48 | "ruleId": "no-unused-vars", 49 | "severity": 2, 50 | "message": "'i' is defined but never used.", 51 | "line": 40, 52 | "column": 5, 53 | "nodeType": "Identifier", 54 | "source": "var i, j;" 55 | }, 56 | { 57 | "ruleId": "no-unused-vars", 58 | "severity": 2, 59 | "message": "'j' is defined but never used.", 60 | "line": 40, 61 | "column": 8, 62 | "nodeType": "Identifier", 63 | "source": "var i, j;" 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /test/fixtures/no-line-numbers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/index.js", 4 | "messages": [ 5 | { 6 | "ruleId": "no-warning-comments", 7 | "severity": 1, 8 | "message": "Unexpected 'todo' comment.", 9 | "nodeType": "Line", 10 | "source": "\t// TODO: fix this later" 11 | }, 12 | { 13 | "ruleId": "no-multiple-empty-lines", 14 | "severity": 2, 15 | "message": "More than 1 blank line not allowed.", 16 | "nodeType": "Program", 17 | "source": "" 18 | } 19 | ], 20 | "errorCount": 1, 21 | "warningCount": 1 22 | }, 23 | { 24 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/test.js", 25 | "messages": [ 26 | { 27 | "ruleId": "ava/use-test", 28 | "severity": 2, 29 | "message": "AVA should be imported as `test`.", 30 | "nodeType": "ImportDeclaration", 31 | "source": "import ava from 'ava';" 32 | } 33 | ], 34 | "errorCount": 1, 35 | "warningCount": 0 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /test/fixtures/sort-by-severity-then-line-then-column.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "filePath": "/Users/sindresorhus/dev/eslint-formatter-pretty/index.js", 4 | "messages": [ 5 | { 6 | "ruleId": "no-warning-comments", 7 | "severity": 1, 8 | "message": "Unexpected 'todo' comment.", 9 | "line": 1, 10 | "column": 1, 11 | "nodeType": "Line", 12 | "source": "\t// TODO: fix this later" 13 | }, 14 | { 15 | "ruleId": "no-multiple-empty-lines", 16 | "severity": 2, 17 | "message": "More than 1 blank line not allowed.", 18 | "line": 3, 19 | "column": 1, 20 | "nodeType": "Program", 21 | "source": "" 22 | }, 23 | { 24 | "ruleId": "no-warning-comments", 25 | "severity": 1, 26 | "message": "Unexpected 'todo' comment.", 27 | "line": 10, 28 | "column": 2, 29 | "nodeType": "Line", 30 | "source": "\t// TODO: fix this later" 31 | }, 32 | { 33 | "ruleId": "no-multiple-empty-lines", 34 | "severity": 2, 35 | "message": "More than 1 blank line not allowed.", 36 | "line": 30, 37 | "column": 1, 38 | "nodeType": "Program", 39 | "source": "" 40 | }, 41 | { 42 | "ruleId": "no-unused-vars", 43 | "severity": 2, 44 | "message": "'i' is defined but never used.", 45 | "line": 40, 46 | "column": 5, 47 | "nodeType": "Identifier", 48 | "source": "var i, j;" 49 | }, 50 | { 51 | "ruleId": "no-unused-vars", 52 | "severity": 2, 53 | "message": "'j' is defined but never used.", 54 | "line": 40, 55 | "column": 8, 56 | "nodeType": "Identifier", 57 | "source": "var i, j;" 58 | } 59 | ], 60 | "errorCount": 4, 61 | "warningCount": 2 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint "ava/no-import-test-files": "off" */ 2 | import {createRequire} from 'node:module'; 3 | import process from 'node:process'; 4 | import test from 'ava'; 5 | import stripAnsi from 'strip-ansi'; 6 | import ansiEscapes from 'ansi-escapes'; 7 | import chalk from 'chalk'; 8 | import eslintFormatterPretty from '../index.js'; // eslint-disable-line import/order 9 | 10 | /// import defaultFixture from './fixtures/default.json'; 11 | // import noLineNumbers from './fixtures/no-line-numbers.json'; 12 | // import lineNumbers from './fixtures/line-numbers.json'; 13 | // import sortOrder from './fixtures/sort-by-severity-then-line-then-column.json'; 14 | // import messages from './fixtures/messages.json'; 15 | // import data from './fixtures/data.json'; 16 | 17 | const require = createRequire(import.meta.url); 18 | 19 | const defaultFixture = require('./fixtures/default.json'); 20 | const noLineNumbers = require('./fixtures/no-line-numbers.json'); 21 | const lineNumbers = require('./fixtures/line-numbers.json'); 22 | const sortOrder = require('./fixtures/sort-by-severity-then-line-then-column.json'); 23 | const messages = require('./fixtures/messages.json'); 24 | const data = require('./fixtures/data.json'); 25 | 26 | const fakeMessages = (desiredSeverity, desiredCount) => { 27 | const ofDesiredSeverity = messages.filter(({severity}) => severity === desiredSeverity); 28 | 29 | if (ofDesiredSeverity.length < desiredCount) { 30 | throw new Error( 31 | `requested ${desiredCount} messages with severity ${desiredSeverity}. Only found ${desiredSeverity.length}.`, 32 | ); 33 | } 34 | 35 | return ofDesiredSeverity.slice(0, desiredCount); 36 | }; 37 | 38 | const fakeReport = (errorCount, warningCount) => ({ 39 | filePath: `${errorCount}-error.${warningCount}-warning.js`, 40 | errorCount, 41 | warningCount, 42 | messages: [ 43 | ...fakeMessages(1, warningCount), 44 | ...fakeMessages(2, errorCount), 45 | ], 46 | }); 47 | 48 | const enableHyperlinks = () => { 49 | process.env.FORCE_HYPERLINK = '1'; 50 | }; 51 | 52 | const disableHyperlinks = () => { 53 | process.env.FORCE_HYPERLINK = '0'; 54 | }; 55 | 56 | test('output', t => { 57 | disableHyperlinks(); 58 | const output = eslintFormatterPretty(defaultFixture); 59 | console.log(output); 60 | t.regex(stripAnsi(output), /index\.js:18:2\n/); 61 | t.regex(stripAnsi(output), /✖ {3}1:1 {2}AVA should be imported as test. {6}ava\/use-test/); 62 | }); 63 | 64 | test('file heading links to the first error line', t => { 65 | disableHyperlinks(); 66 | const output = eslintFormatterPretty(defaultFixture); 67 | console.log(output); 68 | t.regex(stripAnsi(output), /index\.js:18:2\n/); 69 | }); 70 | 71 | test('file heading links to the first warning line if no errors in the file', t => { 72 | disableHyperlinks(); 73 | const output = eslintFormatterPretty(defaultFixture); 74 | console.log(output); 75 | t.regex(stripAnsi(output), /test\.js:1:1\n/); 76 | }); 77 | 78 | test('no line numbers', t => { 79 | disableHyperlinks(); 80 | const output = eslintFormatterPretty(noLineNumbers); 81 | console.log(output); 82 | t.regex(stripAnsi(output), /index\.js\n/); 83 | t.regex(stripAnsi(output), /✖ {2}AVA should be imported as test. {6}ava\/use-test/); 84 | }); 85 | 86 | test('show line numbers', t => { 87 | disableHyperlinks(); 88 | const output = eslintFormatterPretty(lineNumbers); 89 | console.log(output); 90 | t.regex(stripAnsi(output), /⚠ {3}0:0 {2}Unexpected todo comment. {13}no-warning-comments/); 91 | t.regex(stripAnsi(output), /✖ {3}1:1 {2}AVA should be imported as test. {6}ava\/use-test/); 92 | }); 93 | 94 | test('link rules to documentation when terminal supports links', t => { 95 | enableHyperlinks(); 96 | const output = eslintFormatterPretty(defaultFixture); 97 | console.log(output); 98 | t.true(output.includes(ansiEscapes.link(chalk.dim('no-warning-comments'), 'https://eslint.org/docs/rules/no-warning-comments'))); 99 | }); 100 | 101 | test('sort by severity, then line number, then column number', t => { 102 | disableHyperlinks(); 103 | const output = eslintFormatterPretty(sortOrder); 104 | const sanitized = stripAnsi(output); 105 | const indexes = [ 106 | sanitized.indexOf('⚠ 1:1'), 107 | sanitized.indexOf('⚠ 10:2'), 108 | sanitized.indexOf('✖ 3:1'), 109 | sanitized.indexOf('✖ 30:1'), 110 | sanitized.indexOf('✖ 40:5'), 111 | sanitized.indexOf('✖ 40:8'), 112 | ]; 113 | console.log(output); 114 | t.deepEqual(indexes, [...indexes].sort((a, b) => a - b)); 115 | }); 116 | 117 | test('display warning total before error total', t => { 118 | disableHyperlinks(); 119 | const output = eslintFormatterPretty(sortOrder); 120 | const sanitized = stripAnsi(output); 121 | const indexes = [ 122 | sanitized.indexOf('2 warnings'), 123 | sanitized.indexOf('4 errors'), 124 | ]; 125 | console.log(output); 126 | t.deepEqual(indexes, [...indexes].sort((a, b) => a - b)); 127 | }); 128 | 129 | test('files will be sorted with least errors at the bottom, but zero errors at the top', t => { 130 | disableHyperlinks(); 131 | const reports = [ 132 | fakeReport(1, 0), 133 | fakeReport(3, 0), 134 | fakeReport(0, 1), 135 | fakeReport(2, 2), 136 | ]; 137 | const output = eslintFormatterPretty(reports); 138 | const sanitized = stripAnsi(output); 139 | const indexes = [ 140 | sanitized.indexOf('0-error.1-warning.js'), 141 | sanitized.indexOf('3-error.0-warning.js'), 142 | sanitized.indexOf('2-error.2-warning.js'), 143 | sanitized.indexOf('1-error.0-warning.js'), 144 | ]; 145 | console.log(output); 146 | t.is(indexes.length, reports.length); 147 | t.deepEqual(indexes, [...indexes].sort((a, b) => a - b)); 148 | }); 149 | 150 | test('files with similar errorCounts will sort according to warningCounts', t => { 151 | disableHyperlinks(); 152 | const reports = [ 153 | fakeReport(1, 0), 154 | fakeReport(1, 2), 155 | fakeReport(1, 1), 156 | fakeReport(0, 1), 157 | fakeReport(0, 2), 158 | fakeReport(0, 3), 159 | fakeReport(2, 2), 160 | fakeReport(2, 1), 161 | ]; 162 | const output = eslintFormatterPretty(reports); 163 | const sanitized = stripAnsi(output); 164 | const indexes = [ 165 | sanitized.indexOf('0-error.3-warning.js'), 166 | sanitized.indexOf('0-error.2-warning.js'), 167 | sanitized.indexOf('0-error.1-warning.js'), 168 | sanitized.indexOf('2-error.2-warning.js'), 169 | sanitized.indexOf('2-error.1-warning.js'), 170 | sanitized.indexOf('1-error.2-warning.js'), 171 | sanitized.indexOf('1-error.1-warning.js'), 172 | sanitized.indexOf('1-error.0-warning.js'), 173 | ]; 174 | console.log(output); 175 | t.is(indexes.length, reports.length); 176 | t.deepEqual(indexes, [...indexes].sort((a, b) => a - b)); 177 | }); 178 | 179 | test('use the `rulesMeta` property to get docs URL', t => { 180 | enableHyperlinks(); 181 | const output = eslintFormatterPretty(defaultFixture, data); 182 | console.log(output); 183 | t.true(output.includes(ansiEscapes.link(chalk.dim('no-warning-comments'), 'https://eslint.org/docs/rules/test/no-warning-comments'))); 184 | }); 185 | 186 | test('doesn\'t throw errors when rule docs aren\'t found', t => { 187 | enableHyperlinks(); 188 | const output = eslintFormatterPretty(defaultFixture, data); 189 | console.log(output); 190 | t.true(output.includes('@typescript-eslint/no-unused-vars')); 191 | }); 192 | --------------------------------------------------------------------------------