├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── config.js ├── config.json ├── fail.js ├── fix.js ├── format.js ├── quiet.js ├── result.js └── watch.js ├── gulpfile.js ├── index.js ├── package-lock.json ├── package.json ├── test ├── fail.js ├── fixtures │ └── eslintrc-sharable-config.js ├── format.js ├── linting.js ├── result.js └── util.js └── util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change for personal preference 11 | indent_size = 4 12 | 13 | # Do not change 14 | indent_style = tab 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.js] 23 | curly_bracket_next_line = false 24 | spaces_around_operators = true 25 | indent_brace_style = 1TBS 26 | spaces_around_brackets = both 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/ignored.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 7 3 | 4 | env: 5 | node: true 6 | 7 | rules: 8 | indent: [1, 'tab'] 9 | brace-style: [2, "1tbs"] 10 | comma-style: [1, "last"] 11 | default-case: 2 12 | func-style: [2, "declaration"] 13 | no-floating-decimal: 2 14 | no-nested-ternary: 2 15 | radix: 2 16 | space-before-function-paren: [1, "never"] 17 | keyword-spacing: [2, {after: true}] 18 | space-before-blocks: 1 19 | spaced-comment: [2, "always", { exceptions: ["-"]}] 20 | valid-jsdoc: [1, { requireReturn: false, prefer: { return: "returns" }}] 21 | wrap-iife: 2 22 | guard-for-in: 2 23 | strict: [2, "global"] 24 | no-alert: 2 25 | camelcase: 1 26 | curly: [2, "all"] 27 | eqeqeq: [2, "allow-null"] 28 | no-empty: 2 29 | no-use-before-define: 2 30 | no-obj-calls: 2 31 | no-unused-vars: [1, {vars: "local", args: "after-used"}] 32 | new-cap: 2 33 | no-shadow: 1 34 | no-invalid-regexp: 2 35 | comma-dangle: [1, "never"] 36 | no-undef: 2 37 | no-new: 2 38 | no-extra-semi: 2 39 | no-debugger: 2 40 | no-caller: 1 41 | semi: 2 42 | quotes: [1, "single", "avoid-escape"] 43 | no-unreachable: 2 44 | eol-last: 1 45 | operator-linebreak: [1, "before", {overrides: {"?": "after", ":": "after"}}] 46 | max-len: [1, 98, 4, {"ignoreComments": true}] 47 | no-multi-str: 1 48 | no-mixed-spaces-and-tabs: 1 49 | no-trailing-spaces: 1 50 | space-infix-ops: 1 51 | space-unary-ops: 1 52 | no-with: 2 53 | dot-notation: 1 54 | semi-spacing: 1 55 | key-spacing: [1, {beforeColon: false, afterColon: true, mode: "minimum"}] 56 | space-in-parens: [1, "never"] 57 | no-var: 2 58 | prefer-const: 2 59 | 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | *.sublime-* 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - '8.10.0' 5 | before_install: npm install --global npm 6 | script: npm run-script pretest && npm run-script coverage 7 | after_script: npx coveralls < coverage/lcov.info 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 6.0.0 4 | 5 | * Bump `eslint` dependency to ^6.0.0 6 | * Drop support for Node 6 and 7 7 | 8 | ## 5.0.0 9 | 10 | * Bump `eslint` dependency to ^5.0.0 11 | * Use destructuring assignment to simplify the code 12 | 13 | ## 4.0.2 14 | 15 | * Update `plugin-error` to ^1.0.0 16 | 17 | ## 4.0.1 18 | 19 | * Make `fix` option work even if `quiet` option is also enabled 20 | * Remove deprecated [`gulp-util`](https://github.com/gulpjs/gulp-util) dependency and use individual modules instead 21 | 22 | ## 4.0.0 23 | 24 | * Drop support for linting [`Stream`](https://nodejs.org/api/stream.html#stream_stream) [contents](https://github.com/gulpjs/vinyl#optionscontents) 25 | * Because almost all, at least widely used tools to handle JavaScript files don't support `Streams`. They only support either `String` or `Buffer`. 26 | * Use [`Buffer.from()`](https://nodejs.org/api/buffer.html#buffer_class_method_buffer_from_string_encoding) instead of the deprecated [`new Buffer()`](https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_new_buffer_string_encoding) 27 | * Note that `Buffer.from` is only available on Node.js >= [4.5.0](https://nodejs.org/en/blog/release/v4.5.0/). 28 | * Bump [`eslint`](https://github.com/eslint/eslint) dependency to [`^4.0.0`](https://eslint.org/blog/2017/06/eslint-v4.0.0-released) 29 | * Emit a [`PluginError`](https://github.com/gulpjs/gulp-util#new-pluginerrorpluginname-message-options) when it fails to load an [ESLint plugin](https://eslint.org/docs/user-guide/configuring#configuring-plugins) 30 | 31 | ## 3.0.1 32 | 33 | * Remove unnecessary `object-assign` dependency 34 | 35 | ## 3.0.0 36 | 37 | * Bump eslint dependency to ^3.0.0 38 | * Use ES2015 syntax 39 | * Remove these deprecated option aliases: 40 | * `global` 41 | * `env` 42 | * `config` 43 | * `rulesdir` 44 | * `eslintrc` 45 | * Drop support for non-array `globals` option 46 | 47 | ## 2.1.0 48 | 49 | * Remove now obsolete error handling for formatter loading 50 | * It's gracefully done in ESLint >=v2.10.0. 51 | 52 | ## 2.0.0 53 | 54 | * Update to ESLint 2.0.0, along with other dependency updates 55 | * Replace JSCS with ESLint equivalent rules 56 | 57 | ## 1.1.1 58 | 59 | * Fix config migration of "extends" and "ecmaFeatures" options 60 | 61 | ## 1.1.0 62 | 63 | * Bump eslint dependency to ^1.4.0, when "fix" option was added 64 | * Apply eslint-fixed source to gulp file contents 65 | * Add "quiet" option to filter eslint messages 66 | * Update .eslintignore resolution to match eslint 67 | * Add file ignore warnings behind "warnFileIgnored" option 68 | * Migrate "ecmaFeatures" and "extends" option to "baseConfig" option 69 | * Add "result" and "results" methods and tests 70 | * Refactor "failOnError", "failAfterError", "format", and "formatEach" to use "result" or "results" methods 71 | 72 | ## 1.0.0 73 | 74 | * Bump eslint dependency to ^1.0.0 75 | * Update dev-dependencies and js-doc formats 76 | 77 | ## 0.15.0 78 | 79 | * Update dependencies 80 | * Bump eslint dependency to ^0.24.0 81 | 82 | ## 0.14.0 83 | 84 | * Bump eslint dependency to ^0.23.0 85 | * Remove no-longer-needed code 86 | * Fix project eslintrc syntax 87 | 88 | ## 0.13.2 89 | 90 | * Remove dependency on through2 to address highWatermark overflow issue (#36) 91 | 92 | ## 0.13.1 93 | 94 | * Update dependencies 95 | 96 | ## 0.13.0 97 | 98 | * Bump eslint dependency to ^0.22.1 99 | 100 | ## 0.12.0 101 | 102 | * Bump eslint dependency to 0.21.x 103 | 104 | ## 0.11.1 105 | 106 | * tidying-up dependencies 107 | 108 | ## 0.11.0 109 | 110 | * Improve code coverage 111 | * Remove support for deprecated/legacy formatters 112 | 113 | ## 0.10.0 114 | 115 | * Bump eslint dependency to 0.20.x 116 | 117 | ## 0.9.0 118 | 119 | * Bump eslint dependency to 0.19.x 120 | 121 | ## 0.8.0 122 | 123 | * Bump eslint dependency to 0.18.x 124 | 125 | ## 0.7.0 126 | 127 | * Bump eslint dependency to 0.17.x 128 | 129 | ## 0.6.0 130 | 131 | * Bump eslint dependency to 0.16.x 132 | 133 | ## 0.5.0 134 | 135 | * Bump eslint dependency to 0.15.x 136 | 137 | ## 0.4.3 138 | 139 | * Fix "rulePaths" typo 140 | 141 | ## 0.4.2 142 | 143 | * Bump bufferstreams dependency to 1.x 144 | * Fix wrong option handling (@Jakobo) 145 | 146 | ## 0.4.1 147 | 148 | * Code refactoring 149 | 150 | ## 0.4.0 151 | 152 | * Bump eslint dependency to 0.14.x 153 | * Use Stream2 instead of older Stream 154 | 155 | ## 0.3.0 156 | 157 | * Import filesystem-local config plugins 158 | * Fix doc typo 159 | 160 | ## 0.2.2 161 | 162 | * Upgraded eslint to 0.13.0 163 | * Fix filesystem-local .eslintrc loading 164 | * Fix filesystem-local .eslintignore loading 165 | * Add failAfterError to fail at the end of the stream instead of the first error (works well with 'format' method) 166 | 167 | ## 0.2.1 (unreleased) 168 | 169 | * Upgraded eslint to 0.11.0 170 | 171 | ## 0.2.0 172 | 173 | * WAY overdue upgrade to eslint (^0.9.2) 174 | * Use eslint's CLIEngine module to do most of the configuration work (yay!) 175 | * Semi-Breaking Change: Remove gulpEslint.linter. Linting will occur with compatible, installed version of eslint. 176 | 177 | ## 0.1.8 178 | 179 | * Use "dependencies" instead of "peerDependencies" 180 | * Update .eslintrc to account for new eol-last rule in eslint 0.7.1 181 | * Check for message.severity when evaluating messages in failOnError 182 | 183 | ## 0.1.7 184 | 185 | * Open eslint dependency to future versions 186 | * Cut out several unnecessary dependencies 187 | * Declare eslint as a peer dependency to support variation in version 188 | * Fix support for nodejs 0.11 189 | 190 | ## 0.1.6 191 | 192 | * Update dependencies, include eslint 0.5.0 193 | * Integrate eslint cli-config changes 194 | * Accept string array of environments to enable 195 | * Accept string array of globals ('key:boolean' or 'key') 196 | 197 | ## 0.1.5 198 | 199 | * Do not format when there are no eslint'd files 200 | 201 | ## 0.1.4 202 | 203 | * Update eslint version to 0.4.0 204 | 205 | ## 0.1.3 206 | 207 | * Change default formatter to 'stylish' 208 | * Add support for .eslintignore file 209 | * Skip non-JS files to play well with multi-filetype streams 210 | * Add "failOnError" method to stop streams when an eslint error has occurred 211 | * Use gulp-util's PluginError 212 | * Ignore shebangs in JS files 213 | 214 | ## 0.1.2 215 | 216 | * Update eslint version to 0.3.0 217 | 218 | ## 0.1.1 219 | 220 | * Update dependency versions 221 | * Loosen version peer dependency on Gulp 222 | 223 | ## 0.1.0 224 | 225 | * initial plugin 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 ADAMETRY 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-eslint [![Build Status](https://travis-ci.org/adametry/gulp-eslint.svg)](https://travis-ci.org/adametry/gulp-eslint) [![Coverage Status](https://img.shields.io/coveralls/adametry/gulp-eslint.svg)](https://coveralls.io/github/adametry/gulp-eslint) 2 | 3 | > A [gulp](https://gulpjs.com/) plugin for [ESLint](https://eslint.org/) 4 | 5 | ## Installation 6 | 7 | [Use](https://docs.npmjs.com/cli/install) [npm](https://docs.npmjs.com/getting-started/what-is-npm). 8 | 9 | ``` 10 | npm install gulp-eslint 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```javascript 16 | const {src, task} = require('gulp'); 17 | const eslint = require('gulp-eslint'); 18 | 19 | task('default', () => { 20 | return src(['scripts/*.js']) 21 | // eslint() attaches the lint output to the "eslint" property 22 | // of the file object so it can be used by other modules. 23 | .pipe(eslint()) 24 | // eslint.format() outputs the lint results to the console. 25 | // Alternatively use eslint.formatEach() (see Docs). 26 | .pipe(eslint.format()) 27 | // To have the process exit with an error code (1) on 28 | // lint error, return the stream and pipe to failAfterError last. 29 | .pipe(eslint.failAfterError()); 30 | }); 31 | ``` 32 | 33 | Or use the plugin API to do things like: 34 | 35 | ```javascript 36 | gulp.src(['**/*.js','!node_modules/**']) 37 | .pipe(eslint({ 38 | rules: { 39 | 'my-custom-rule': 1, 40 | 'strict': 2 41 | }, 42 | globals: [ 43 | 'jQuery', 44 | '$' 45 | ], 46 | envs: [ 47 | 'browser' 48 | ] 49 | })) 50 | .pipe(eslint.formatEach('compact', process.stderr)); 51 | ``` 52 | 53 | For additional examples, look through the [example directory](https://github.com/adametry/gulp-eslint/tree/master/example). 54 | 55 | ## API 56 | 57 | ### eslint() 58 | 59 | *No explicit configuration.* A `.eslintrc` file may be resolved relative to each linted file. 60 | 61 | ### eslint(options) 62 | See [ESlint CLIEngine options](https://eslint.org/docs/developer-guide/nodejs-api#cliengine). 63 | 64 | #### options.rules 65 | 66 | Type: `Object` 67 | 68 | Set [configuration](https://eslint.org/docs/user-guide/configuring#configuring-rules) of [rules](https://eslint.org/docs/rules/). 69 | 70 | ```javascript 71 | { 72 | "rules":{ 73 | "camelcase": 1, 74 | "comma-dangle": 2, 75 | "quotes": 0 76 | } 77 | } 78 | ``` 79 | 80 | #### options.globals 81 | 82 | Type: `Array` 83 | 84 | Specify global variables to declare. 85 | 86 | ```javascript 87 | { 88 | "globals":[ 89 | "jQuery", 90 | "$" 91 | ] 92 | } 93 | ``` 94 | 95 | #### options.fix 96 | 97 | Type: `Boolean` 98 | 99 | This option instructs ESLint to try to fix as many issues as possible. The fixes are applied to the gulp stream. The fixed content can be saved to file using `gulp.dest` (See [example/fix.js](https://github.com/adametry/gulp-eslint/blob/master/example/fix.js)). Rules that are fixable can be found in ESLint's [rules list](https://eslint.org/docs/rules/). 100 | 101 | When fixes are applied, a "fixed" property is set to `true` on the fixed file's ESLint result. 102 | 103 | #### options.quiet 104 | 105 | Type: `Boolean` 106 | 107 | When `true`, this option will filter warning messages from ESLint results. This mimics the ESLint CLI [quiet option](https://eslint.org/docs/user-guide/command-line-interface#quiet). 108 | 109 | Type: `function (message, index, list) { return Boolean(); }` 110 | 111 | When provided a function, it will be used to filter ESLint result messages, removing any messages that do not return a `true` (or truthy) value. 112 | 113 | #### options.envs 114 | 115 | Type: `Array` 116 | 117 | Specify a list of [environments](https://eslint.org/docs/user-guide/configuring#specifying-environments) to be applied. 118 | 119 | #### options.rulePaths 120 | 121 | Type: `Array` 122 | 123 | This option allows you to specify additional directories from which to load rules files. This is useful when you have custom rules that aren't suitable for being bundled with ESLint. This option works much like the ESLint CLI's [rulesdir option](https://eslint.org/docs/user-guide/command-line-interface#rulesdir). 124 | 125 | #### options.configFile 126 | 127 | Type: `String` 128 | 129 | Path to the ESLint rules configuration file. For more information, see the ESLint CLI [config option](https://eslint.org/docs/user-guide/command-line-interface#c-config) and [Using Configuration Files](https://eslint.org/docs/user-guide/configuring#using-configuration-files). 130 | 131 | #### options.warnFileIgnored 132 | 133 | Type: `Boolean` 134 | 135 | When `true`, add a result warning when ESLint ignores a file. This can be used to file files that are needlessly being loaded by `gulp.src`. For example, since ESLint automatically ignores "node_modules" file paths and gulp.src does not, a gulp task may take seconds longer just reading files from the "node_modules" directory. 136 | 137 | #### options.useEslintrc 138 | 139 | Type: `Boolean` 140 | 141 | When `false`, ESLint will not load [.eslintrc files](https://eslint.org/docs/user-guide/configuring#using-configuration-files). 142 | 143 | ### eslint(configFilePath) 144 | 145 | Type: `String` 146 | 147 | Shorthand for defining `options.configFile`. 148 | 149 | ### eslint.result(action) 150 | 151 | Type: `function (result) {}` 152 | 153 | Call a function for each ESLint file result. No returned value is expected. If an error is thrown, it will be wrapped in a Gulp PluginError and emitted from the stream. 154 | 155 | ```javascript 156 | gulp.src(['**/*.js','!node_modules/**']) 157 | .pipe(eslint()) 158 | .pipe(eslint.result(result => { 159 | // Called for each ESLint result. 160 | console.log(`ESLint result: ${result.filePath}`); 161 | console.log(`# Messages: ${result.messages.length}`); 162 | console.log(`# Warnings: ${result.warningCount}`); 163 | console.log(`# Errors: ${result.errorCount}`); 164 | })); 165 | ``` 166 | 167 | Type: `function (result, callback) { callback(error); }` 168 | 169 | Call an asynchronous function for each ESLint file result. The callback must be called for the stream to finish. If a value is passed to the callback, it will be wrapped in a Gulp PluginError and emitted from the stream. 170 | 171 | 172 | ### eslint.results(action) 173 | 174 | Type: `function (results) {}` 175 | 176 | Call a function once for all ESLint file results before a stream finishes. No returned value is expected. If an error is thrown, it will be wrapped in a Gulp PluginError and emitted from the stream. 177 | 178 | The results list has a "warningCount" property that is the sum of warnings in all results; likewise, an "errorCount" property is set to the sum of errors in all results. 179 | 180 | ```javascript 181 | gulp.src(['**/*.js','!node_modules/**']) 182 | .pipe(eslint()) 183 | .pipe(eslint.results(results => { 184 | // Called once for all ESLint results. 185 | console.log(`Total Results: ${results.length}`); 186 | console.log(`Total Warnings: ${results.warningCount}`); 187 | console.log(`Total Errors: ${results.errorCount}`); 188 | })); 189 | ``` 190 | 191 | Type: `function (results, callback) { callback(error); }` 192 | 193 | Call an asynchronous function once for all ESLint file results before a stream finishes. The callback must be called for the stream to finish. If a value is passed to the callback, it will be wrapped in a Gulp PluginError and emitted from the stream. 194 | 195 | ### eslint.failOnError() 196 | 197 | Stop a task/stream if an ESLint error has been reported for any file. 198 | 199 | ```javascript 200 | // Cause the stream to stop(/fail) before copying an invalid JS file to the output directory 201 | gulp.src(['**/*.js','!node_modules/**']) 202 | .pipe(eslint()) 203 | .pipe(eslint.failOnError()); 204 | ``` 205 | 206 | ### eslint.failAfterError() 207 | 208 | Stop a task/stream if an ESLint error has been reported for any file, but wait for all of them to be processed first. 209 | 210 | ```javascript 211 | // Cause the stream to stop(/fail) when the stream ends if any ESLint error(s) occurred. 212 | gulp.src(['**/*.js','!node_modules/**']) 213 | .pipe(eslint()) 214 | .pipe(eslint.failAfterError()); 215 | ``` 216 | 217 | ### eslint.format(formatter, output) 218 | 219 | Format all linted files once. This should be used in the stream after piping through `eslint`; otherwise, this will find no ESLint results to format. 220 | 221 | The `formatter` argument may be a `String`, `Function`, or `undefined`. As a `String`, a formatter module by that name or path will be resolved as a module, relative to `process.cwd()`, or as one of the [ESLint-provided formatters](https://github.com/eslint/eslint/tree/master/lib/formatters). If `undefined`, the ESLint “stylish” formatter will be resolved. A `Function` will be called with an `Array` of file linting results to format. 222 | 223 | ```javascript 224 | // use the default "stylish" ESLint formatter 225 | eslint.format() 226 | 227 | // use the "checkstyle" ESLint formatter 228 | eslint.format('checkstyle') 229 | 230 | // use the "eslint-path-formatter" module formatter 231 | // (@see https://github.com/Bartvds/eslint-path-formatter) 232 | eslint.format('node_modules/eslint-path-formatter') 233 | ``` 234 | 235 | The `output` argument may be a `WritableStream`, `Function`, or `undefined`. As a `WritableStream`, the formatter results will be written to the stream. If `undefined`, the formatter results will be written to [gulp’s log](https://github.com/gulpjs/gulp-util#logmsg). A `Function` will be called with the formatter results as the only parameter. 236 | 237 | ```javascript 238 | // write to gulp's log (default) 239 | eslint.format(); 240 | 241 | // write messages to stdout 242 | eslint.format('junit', process.stdout) 243 | ``` 244 | 245 | ### eslint.formatEach(formatter, output) 246 | 247 | Format each linted file individually. This should be used in the stream after piping through `eslint`; otherwise, this will find no ESLint results to format. 248 | 249 | The arguments for `formatEach` are the same as the arguments for `format`. 250 | 251 | 252 | ## Configuration 253 | 254 | ESLint may be configured explicity by using any of the following plugin options: `config`, `rules`, `globals`, or `env`. If the [useEslintrc option](#useEslintrc) is not set to `false`, ESLint will attempt to resolve a file by the name of `.eslintrc` within the same directory as the file to be linted. If not found there, parent directories will be searched until `.eslintrc` is found or the directory root is reached. 255 | 256 | ## Ignore Files 257 | 258 | ESLint will ignore files that do not have a `.js` file extension at the point of linting ([some plugins](https://github.com/contra/gulp-coffee) may change file extensions mid-stream). This avoids unintentional linting of non-JavaScript files. 259 | 260 | ESLint will also detect an `.eslintignore` file at the cwd or a parent directory. See the [ESLint docs](https://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories) to learn how to construct this file. 261 | 262 | ## Extensions 263 | 264 | ESLint results are attached as an "eslint" property to the vinyl files that pass through a Gulp.js stream pipeline. This is available to streams that follow the initial `eslint` stream. The [eslint.result](#result) and [eslint.results](#results) methods are made available to support extensions and custom handling of ESLint results. 265 | 266 | #### Gulp-Eslint Extensions: 267 | 268 | * [gulp-eslint-if-fixed](https://github.com/lukeapage/gulp-eslint-if-fixed) 269 | * [gulp-eslint-threshold](https://github.com/krmbkt/gulp-eslint-threshold) 270 | -------------------------------------------------------------------------------- /example/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint 4 | 5 | const {src, task} = require('gulp'); 6 | const eslint = require('..'); 7 | 8 | /** 9 | * Simple example of using ESLint and a formatter 10 | * Note: ESLint does not write to the console itself. 11 | * Use format or formatEach to print ESLint results. 12 | * @returns {stream} gulp file stream 13 | */ 14 | task('basic', () => { 15 | return src('../test/fixtures/**/*.js') 16 | // default: use local linting config 17 | .pipe(eslint()) 18 | // format ESLint results and print them to the console 19 | .pipe(eslint.format()); 20 | }); 21 | 22 | /** 23 | * Inline ESLint configuration 24 | * @returns {stream} gulp file stream 25 | */ 26 | task('inline-config', () => { 27 | return src('../test/fixtures/**/*.js') 28 | .pipe(eslint({ 29 | rules: { 30 | 'no-alert': 0, 31 | 'no-bitwise': 0, 32 | 'camelcase': 1, 33 | 'curly': 1, 34 | 'eqeqeq': 0, 35 | 'no-eq-null': 0, 36 | 'guard-for-in': 1, 37 | 'no-empty': 1, 38 | 'no-use-before-define': 0, 39 | 'no-obj-calls': 2, 40 | 'no-unused-vars': 0, 41 | 'new-cap': 1, 42 | 'no-shadow': 0, 43 | 'strict': 2, 44 | 'no-invalid-regexp': 2, 45 | 'comma-dangle': 2, 46 | 'no-undef': 1, 47 | 'no-new': 1, 48 | 'no-extra-semi': 1, 49 | 'no-debugger': 2, 50 | 'no-caller': 1, 51 | 'semi': 1, 52 | 'quotes': 0, 53 | 'no-unreachable': 2 54 | }, 55 | 56 | globals: ['$'], 57 | 58 | envs: ['node'] 59 | })) 60 | .pipe(eslint.format()); 61 | }); 62 | 63 | /** 64 | * Load configuration file 65 | * @returns {stream} gulp file stream 66 | */ 67 | task('load-config', () => { 68 | return src('../test/fixtures/**/*.js') 69 | .pipe(eslint({ 70 | // Load a specific ESLint config 71 | configFile: 'config.json' 72 | })) 73 | .pipe(eslint.format()); 74 | }); 75 | 76 | /** 77 | * Shorthand way to load a configuration file 78 | * @returns {stream} gulp file stream 79 | */ 80 | task('load-config-shorthand', () => { 81 | return src('../test/fixtures/**/*.js') 82 | // Load a specific ESLint config 83 | .pipe(eslint('config.json')) 84 | .pipe(eslint.format()); 85 | }); 86 | 87 | /** 88 | * The default task will run all above tasks 89 | */ 90 | task('default', [ 91 | 'basic', 92 | 'inline-config', 93 | 'load-config', 94 | 'load-config-shorthand' 95 | 96 | ], () => { 97 | console.log('All tasks completed successfully.'); 98 | }); 99 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "rules": { 4 | "no-alert": 0, 5 | "no-bitwise": 0, 6 | "camelcase": 1, 7 | "curly": 1, 8 | "eqeqeq": 0, 9 | "no-eq-null": 0, 10 | "guard-for-in": 1, 11 | "no-empty": 1 12 | }, 13 | "globals": { 14 | "$": false 15 | }, 16 | "env": { 17 | "browser": true, 18 | "node": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/fail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint 4 | 5 | const {src, task} = require('gulp'); 6 | const fancyLog = require('fancy-log'); 7 | const eslint = require('../'); 8 | 9 | task('fail-immediately', () => { 10 | return src('../test/fixtures/**/*.js') 11 | .pipe(eslint()) 12 | // format one at time since this stream may fail before it can format them all at the end 13 | .pipe(eslint.formatEach()) 14 | // failOnError will emit an error (fail) immediately upon the first file that has an error 15 | .pipe(eslint.failOnError()) 16 | // need to do something before the process exits? Try this: 17 | .on('error', error => { 18 | fancyLog('Stream Exiting With Error: ' + error.message); 19 | }); 20 | }); 21 | 22 | task('fail-at-end', () => { 23 | return src('../test/fixtures/**/*.js') 24 | .pipe(eslint()) 25 | // Format all results at once, at the end 26 | .pipe(eslint.format()) 27 | // failAfterError will emit an error (fail) just before the stream finishes if any file has an error 28 | .pipe(eslint.failAfterError()); 29 | }); 30 | 31 | task('default', ['fail-immediately']); 32 | -------------------------------------------------------------------------------- /example/fix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint gulp-if 4 | 5 | const {dest, src, task} = require('gulp'); 6 | const gulpIf = require('gulp-if'); 7 | const eslint = require('..'); 8 | 9 | function isFixed(file) { 10 | return file.eslint != null && file.eslint.fixed; 11 | } 12 | 13 | task('lint-n-fix', () => { 14 | return src('../test/fixtures/*.js') 15 | .pipe(eslint({fix: true})) 16 | .pipe(eslint.format()) 17 | // if fixed, write the file to dest 18 | .pipe(gulpIf(isFixed, dest('../test/fixtures'))); 19 | }); 20 | 21 | task('flag-n-fix', () => { 22 | const hasFixFlag = process.argv.slice(2).includes('--fix'); 23 | 24 | return src('../test/fixtures/*.js') 25 | .pipe(eslint({fix: hasFixFlag})) 26 | .pipe(eslint.format()) 27 | // if fixed, write the file to dest 28 | .pipe(gulpIf(isFixed, dest('../test/fixtures'))); 29 | }); 30 | 31 | task('default', ['lint-n-fix']); 32 | -------------------------------------------------------------------------------- /example/format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint 4 | 5 | const {src, task} = require('gulp'); 6 | const eslint = require('..'); 7 | 8 | task('eslint-formatter', () => { 9 | // lint each file, and format all files at once (mul) 10 | return src('../test/fixtures/**/*.js') 11 | .pipe(eslint()) 12 | // use eslint's default formatter by default 13 | .pipe(eslint.format()) 14 | // Name a built-in formatter or path load. 15 | // https://eslint.org/docs/user-guide/command-line-interface#-f---format 16 | .pipe(eslint.format('compact')); 17 | }); 18 | 19 | task('custom-formatter', () => { 20 | function embolden(text) { 21 | return `\u001b[1m${text}\u001b[22m `; 22 | } 23 | 24 | function pluralish(count, text) { 25 | return `${count} ${text}${count === 1 ? '' : 's'}`; 26 | } 27 | 28 | return src('../test/fixtures/**/*.js') 29 | .pipe(eslint()) 30 | .pipe(eslint.format(results => { 31 | 32 | // return formatted text to display 33 | return embolden('[Custom ESLint Summary]') 34 | + pluralish(results.length, 'File') + ', ' 35 | + pluralish(results.errorCount, 'Error') + ', and ' 36 | + pluralish(results.warningCount, 'Warning'); 37 | })); 38 | }); 39 | 40 | task('default', ['eslint-formatter','custom-formatter']); 41 | -------------------------------------------------------------------------------- /example/quiet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint 4 | 5 | const {src, task} = require('gulp'); 6 | const eslint = require('..'); 7 | 8 | task('quiet-lint', () => { 9 | return src('../test/fixtures/*.js') 10 | .pipe(eslint({quiet: true})) 11 | .pipe(eslint.format()); 12 | }); 13 | 14 | function isWarning(message) { 15 | return message.severity === 1; 16 | } 17 | 18 | task('lint-warnings', () => { 19 | return src('../test/fixtures/*.js') 20 | .pipe(eslint({quiet: isWarning})) 21 | .pipe(eslint.format()); 22 | }); 23 | 24 | task('default', ['quiet-lint','lint-warnings']); 25 | -------------------------------------------------------------------------------- /example/result.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint 4 | 5 | const {src, task} = require('gulp'); 6 | const eslint = require('..'); 7 | 8 | const MAX_WARNINGS = 1; 9 | 10 | task('lint-result', () => { 11 | const count = 0; 12 | 13 | // Be sure to return the stream; otherwise, you may not get a proper exit code. 14 | return src('../test/fixtures/*.js') 15 | .pipe(eslint()) 16 | .pipe(eslint.formatEach()) 17 | .pipe(eslint.result(result => { 18 | count += result.warningCount; 19 | 20 | if (count > MAX_WARNINGS) { 21 | // Report which file exceeded the limit 22 | // The error will be wraped in a gulp PluginError 23 | throw { 24 | name: 'TooManyWarnings', 25 | fileName: result.filePath, 26 | message: 'Too many warnings!', 27 | showStack: false 28 | }; 29 | } 30 | })); 31 | }); 32 | 33 | task('lint-resu1lt-async', () => { 34 | let count = 0; 35 | 36 | return src('../test/fixtures/*.js') 37 | .pipe(eslint()) 38 | .pipe(eslint.formatEach()) 39 | .pipe(eslint.result((result, done) => { 40 | // As a basic example, we'll use process.nextTick as an async process. 41 | process.nextTick(function asyncStub() { 42 | count += result.warningCount; 43 | 44 | let error = null; 45 | if (count > MAX_WARNINGS) { 46 | // Define the error. Any non-null/undefined value will work 47 | error = { 48 | name: 'TooManyWarnings', 49 | fileName: result.filePath, 50 | message: 'Too many warnings!', 51 | showStack: false 52 | }; 53 | } 54 | done(error); 55 | }, 100); 56 | })); 57 | }); 58 | 59 | task('lint-results', () => { 60 | return src('../test/fixtures/*.js') 61 | .pipe(eslint()) 62 | .pipe(eslint.format()) 63 | .pipe(eslint.results(results => { 64 | // results.warningCount is an array of file result 65 | // that includes warningsCount and errorCount totals. 66 | if (results.warningCount > MAX_WARNINGS) { 67 | // No specific file to complain about here. 68 | throw new Error('Too many warnings!'); 69 | } 70 | })); 71 | }); 72 | 73 | task('lint-results-async', () => { 74 | return src('../test/fixtures/*.js') 75 | .pipe(eslint()) 76 | .pipe(eslint.format()) 77 | .pipe(eslint.results((results, done) => { 78 | // Another async example... 79 | process.nextTick(function asyncStub() { 80 | const error = null; 81 | if (results.warningCount > MAX_WARNINGS) { 82 | error = new Error('Too many warnings!'); 83 | } 84 | done(error); 85 | 86 | }, 100); 87 | })); 88 | }); 89 | 90 | task('default', ['lint-results']); 91 | -------------------------------------------------------------------------------- /example/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // npm install gulp@next gulp-eslint gulp-cached 4 | 5 | const {src, task, watch} = require('gulp'); 6 | const {resolve} = require('path'); 7 | const eslint = require('..'); 8 | const cache = require('gulp-cached'); 9 | 10 | task('lint-watch', () => { 11 | // Lint only files that change after this watch starts 12 | const lintAndPrint = eslint(); 13 | // format results with each file, since this stream won't end. 14 | lintAndPrint.pipe(eslint.formatEach()); 15 | 16 | return watch('../test/fixtures/*.js', event => { 17 | if (event.type !== 'deleted') { 18 | src(event.path) 19 | .pipe(lintAndPrint, {end: false}); 20 | } 21 | }); 22 | }); 23 | 24 | task('cached-lint', () => { 25 | // Read all js files within test/fixtures 26 | return src('../test/fixtures/*.js') 27 | .pipe(cache('eslint')) 28 | // Only uncached and changed files past this point 29 | .pipe(eslint()) 30 | .pipe(eslint.format()) 31 | .pipe(eslint.result(result => { 32 | if (result.warningCount > 0 || result.errorCount > 0) { 33 | // If a file has errors/warnings remove uncache it 34 | delete cache.caches.eslint[resolve(result.filePath)]; 35 | } 36 | })); 37 | }); 38 | 39 | // Run the "cached-lint" task initially... 40 | task('cached-lint-watch', ['cached-lint'], () => { 41 | // ...and whenever a watched file changes 42 | return watch('../test/fixtures/*.js', ['cached-lint'], event => { 43 | if (event.type === 'deleted' && cache.caches.eslint) { 44 | // remove deleted files from cache 45 | delete cache.caches.eslint[event.path]; 46 | } 47 | }); 48 | }); 49 | 50 | task('default', ['cached-lint-watch']); 51 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {src, task} = require('gulp'); 4 | const eslint = require('.'); 5 | 6 | task('default', () => { 7 | return src(['**/*.js', '!node_modules/**', '!coverage/**', '!test/fixtures/**']) 8 | .pipe(eslint()) 9 | .pipe(eslint.format()) 10 | .pipe(eslint.failAfterError()); 11 | }); 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PluginError = require('plugin-error'); 4 | const {CLIEngine} = require('eslint'); 5 | const { 6 | createIgnoreResult, 7 | filterResult, 8 | firstResultMessage, 9 | handleCallback, 10 | isErrorMessage, 11 | migrateOptions, 12 | resolveFormatter, 13 | resolveWritable, 14 | transform, 15 | tryResultAction, 16 | writeResults 17 | } = require('./util'); 18 | const {relative} = require('path'); 19 | 20 | /** 21 | * Append ESLint result to each file 22 | * 23 | * @param {(Object|String)} [options] - Configure rules, env, global, and other options for running ESLint 24 | * @returns {stream} gulp file stream 25 | */ 26 | function gulpEslint(options) { 27 | options = migrateOptions(options) || {}; 28 | const linter = new CLIEngine(options); 29 | 30 | return transform((file, enc, cb) => { 31 | const filePath = relative(process.cwd(), file.path); 32 | 33 | if (file.isNull()) { 34 | cb(null, file); 35 | return; 36 | } 37 | 38 | if (file.isStream()) { 39 | cb(new PluginError('gulp-eslint', 'gulp-eslint doesn\'t support vinyl files with Stream contents.')); 40 | return; 41 | } 42 | 43 | if (linter.isPathIgnored(filePath)) { 44 | // Note: 45 | // Vinyl files can have an independently defined cwd, but ESLint works relative to `process.cwd()`. 46 | // (https://github.com/gulpjs/gulp/blob/master/docs/recipes/specifying-a-cwd.md) 47 | // Also, ESLint doesn't adjust file paths relative to an ancestory .eslintignore path. 48 | // E.g., If ../.eslintignore has "foo/*.js", ESLint will ignore ./foo/*.js, instead of ../foo/*.js. 49 | // Eslint rolls this into `CLIEngine.executeOnText`. So, gulp-eslint must account for this limitation. 50 | 51 | if (linter.isPathIgnored(filePath) && options.warnFileIgnored) { 52 | // Warn that gulp.src is needlessly reading files that ESLint ignores 53 | file.eslint = createIgnoreResult(file); 54 | } 55 | cb(null, file); 56 | return; 57 | } 58 | 59 | let result; 60 | 61 | try { 62 | result = linter.executeOnText(file.contents.toString(), filePath).results[0]; 63 | } catch (e) { 64 | cb(new PluginError('gulp-eslint', e)); 65 | return; 66 | } 67 | // Note: Fixes are applied as part of "executeOnText". 68 | // Any applied fix messages have been removed from the result. 69 | 70 | if (options.quiet) { 71 | // ignore warnings 72 | file.eslint = filterResult(result, options.quiet); 73 | } else { 74 | file.eslint = result; 75 | } 76 | 77 | // Update the fixed output; otherwise, fixable messages are simply ignored. 78 | if (file.eslint.hasOwnProperty('output')) { 79 | file.contents = Buffer.from(file.eslint.output); 80 | file.eslint.fixed = true; 81 | } 82 | cb(null, file); 83 | }); 84 | } 85 | 86 | /** 87 | * Handle each ESLint result as it passes through the stream. 88 | * 89 | * @param {Function} action - A function to handle each ESLint result 90 | * @returns {stream} gulp file stream 91 | */ 92 | gulpEslint.result = action => { 93 | if (typeof action !== 'function') { 94 | throw new Error('Expected callable argument'); 95 | } 96 | 97 | return transform((file, enc, done) => { 98 | if (file.eslint) { 99 | tryResultAction(action, file.eslint, handleCallback(done, file)); 100 | } else { 101 | done(null, file); 102 | } 103 | }); 104 | }; 105 | 106 | /** 107 | * Handle all ESLint results at the end of the stream. 108 | * 109 | * @param {Function} action - A function to handle all ESLint results 110 | * @returns {stream} gulp file stream 111 | */ 112 | gulpEslint.results = function(action) { 113 | if (typeof action !== 'function') { 114 | throw new Error('Expected callable argument'); 115 | } 116 | 117 | const results = []; 118 | results.errorCount = 0; 119 | results.warningCount = 0; 120 | 121 | return transform((file, enc, done) => { 122 | if (file.eslint) { 123 | results.push(file.eslint); 124 | // collect total error/warning count 125 | results.errorCount += file.eslint.errorCount; 126 | results.warningCount += file.eslint.warningCount; 127 | } 128 | done(null, file); 129 | 130 | }, done => { 131 | tryResultAction(action, results, handleCallback(done)); 132 | }); 133 | }; 134 | 135 | /** 136 | * Fail when an ESLint error is found in ESLint results. 137 | * 138 | * @returns {stream} gulp file stream 139 | */ 140 | gulpEslint.failOnError = () => { 141 | return gulpEslint.result(result => { 142 | const error = firstResultMessage(result, isErrorMessage); 143 | if (!error) { 144 | return; 145 | } 146 | 147 | throw new PluginError('gulp-eslint', { 148 | name: 'ESLintError', 149 | fileName: result.filePath, 150 | message: error.message, 151 | lineNumber: error.line 152 | }); 153 | }); 154 | }; 155 | 156 | /** 157 | * Fail when the stream ends if any ESLint error(s) occurred 158 | * 159 | * @returns {stream} gulp file stream 160 | */ 161 | gulpEslint.failAfterError = () => { 162 | return gulpEslint.results(results => { 163 | const count = results.errorCount; 164 | if (!count) { 165 | return; 166 | } 167 | 168 | throw new PluginError('gulp-eslint', { 169 | name: 'ESLintError', 170 | message: 'Failed with ' + count + (count === 1 ? ' error' : ' errors') 171 | }); 172 | }); 173 | }; 174 | 175 | /** 176 | * Format the results of each file individually. 177 | * 178 | * @param {(String|Function)} [formatter=stylish] - The name or function for a ESLint result formatter 179 | * @param {(Function|Stream)} [writable=fancy-log] - A funtion or stream to write the formatted ESLint results. 180 | * @returns {stream} gulp file stream 181 | */ 182 | gulpEslint.formatEach = (formatter, writable) => { 183 | formatter = resolveFormatter(formatter); 184 | writable = resolveWritable(writable); 185 | 186 | return gulpEslint.result(result => writeResults([result], formatter, writable)); 187 | }; 188 | 189 | /** 190 | * Wait until all files have been linted and format all results at once. 191 | * 192 | * @param {(String|Function)} [formatter=stylish] - The name or function for a ESLint result formatter 193 | * @param {(Function|stream)} [writable=fancy-log] - A funtion or stream to write the formatted ESLint results. 194 | * @returns {stream} gulp file stream 195 | */ 196 | gulpEslint.format = (formatter, writable) => { 197 | formatter = resolveFormatter(formatter); 198 | writable = resolveWritable(writable); 199 | 200 | return gulpEslint.results(results => { 201 | // Only format results if files has been lint'd 202 | if (results.length) { 203 | writeResults(results, formatter, writable); 204 | } 205 | }); 206 | }; 207 | 208 | module.exports = gulpEslint; 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-eslint", 3 | "version": "6.0.0", 4 | "description": "A gulp plugin for processing files with ESLint", 5 | "repository": "adametry/gulp-eslint", 6 | "files": [ 7 | "index.js", 8 | "util.js" 9 | ], 10 | "directories": { 11 | "example": "example", 12 | "test": "test" 13 | }, 14 | "scripts": { 15 | "pretest": "gulp", 16 | "test": "mocha", 17 | "gulp": "gulp", 18 | "gulp-example": "gulp --gulpfile=example/config.js", 19 | "coverage": "istanbul cover _mocha" 20 | }, 21 | "keywords": [ 22 | "gulpplugin", 23 | "eslint", 24 | "gulp", 25 | "errors", 26 | "warnings", 27 | "check", 28 | "source", 29 | "code", 30 | "formatter", 31 | "js", 32 | "javascript", 33 | "task", 34 | "lint", 35 | "plugin" 36 | ], 37 | "author": "Adametry", 38 | "contributors": [ 39 | "Shinnosuke Watanabe (https://github.com/shinnn)" 40 | ], 41 | "license": "MIT", 42 | "dependencies": { 43 | "eslint": "^6.0.0", 44 | "fancy-log": "^1.3.2", 45 | "plugin-error": "^1.0.1" 46 | }, 47 | "devDependencies": { 48 | "@shinnn/eslint-config-node": "^5.0.0", 49 | "babel-eslint": "^8.2.5", 50 | "from2-string": "^1.1.0", 51 | "gulp": "^4.0.0", 52 | "istanbul": "^0.4.5", 53 | "mocha": "^5.2.0", 54 | "should": "^13.2.1", 55 | "vinyl": "^2.2.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/fail.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 'use strict'; 3 | 4 | const File = require('vinyl'); 5 | const path = require('path'); 6 | const should = require('should'); 7 | const eslint = require('../'); 8 | 9 | require('mocha'); 10 | 11 | describe('gulp-eslint failOnError', () => { 12 | it('should fail a file immediately if an error is found', done => { 13 | const lintStream = eslint({useEslintrc: false, rules: {'no-undef': 2}}); 14 | 15 | function endWithoutError() { 16 | done(new Error('An error was not thrown before ending')); 17 | } 18 | 19 | lintStream.pipe(eslint.failOnError()) 20 | .on('error', function(err) { 21 | this.removeListener('finish', endWithoutError); 22 | should.exists(err); 23 | err.message.should.equal('\'x\' is not defined.'); 24 | err.fileName.should.equal(path.resolve('test/fixtures/invalid.js')); 25 | err.plugin.should.equal('gulp-eslint'); 26 | done(); 27 | }) 28 | .on('finish', endWithoutError); 29 | 30 | lintStream.write(new File({ 31 | path: 'test/fixtures/invalid.js', 32 | contents: Buffer.from('x = 1;') 33 | })); 34 | 35 | lintStream.end(); 36 | }); 37 | 38 | it('should pass a file if only warnings are found', done => { 39 | 40 | const lintStream = eslint({useEslintrc: false, rules: {'no-undef': 1, 'strict': 0}}); 41 | 42 | lintStream.pipe(eslint.failOnError()) 43 | .on('error', done) 44 | .on('finish', done); 45 | 46 | lintStream.end(new File({ 47 | path: 'test/fixtures/invalid.js', 48 | contents: Buffer.from('x = 0;') 49 | })); 50 | }); 51 | 52 | it('should handle ESLint reports without messages', done => { 53 | 54 | const file = new File({ 55 | path: 'test/fixtures/invalid.js', 56 | contents: Buffer.from('#invalid!syntax}') 57 | }); 58 | file.eslint = {}; 59 | 60 | eslint.failOnError() 61 | .on('error', (err) => { 62 | this.removeListener('finish', done); 63 | done(err); 64 | }) 65 | .on('finish', done) 66 | .end(file); 67 | }); 68 | 69 | }); 70 | 71 | describe('gulp-eslint failAfterError', () => { 72 | 73 | it('should fail when the file stream ends if an error is found', done => { 74 | const lintStream = eslint({useEslintrc: false, rules: {'no-undef': 2}}); 75 | 76 | function endWithoutError() { 77 | done(new Error('An error was not thrown before ending')); 78 | } 79 | 80 | lintStream.pipe(eslint.failAfterError()) 81 | .on('error', function(err) { 82 | this.removeListener('finish', endWithoutError); 83 | should.exists(err); 84 | err.message.should.equal('Failed with 1 error'); 85 | err.name.should.equal('ESLintError'); 86 | err.plugin.should.equal('gulp-eslint'); 87 | done(); 88 | }) 89 | .on('finish', endWithoutError); 90 | 91 | lintStream.end(new File({ 92 | path: 'test/fixtures/invalid.js', 93 | contents: Buffer.from('x = 1;') 94 | })); 95 | }); 96 | 97 | it('should fail when the file stream ends if multiple errors are found', done => { 98 | const lintStream = eslint({useEslintrc: false, rules: {'no-undef': 2}}); 99 | 100 | lintStream.pipe(eslint.failAfterError().on('error', (err) => { 101 | should.exists(err); 102 | err.message.should.equal('Failed with 2 errors'); 103 | err.name.should.equal('ESLintError'); 104 | err.plugin.should.equal('gulp-eslint'); 105 | done(); 106 | })); 107 | 108 | lintStream.end(new File({ 109 | path: 'test/fixtures/invalid.js', 110 | contents: Buffer.from('x = 1; a = false;') 111 | })); 112 | }); 113 | 114 | it('should pass when the file stream ends if only warnings are found', done => { 115 | const lintStream = eslint({useEslintrc: false, rules: {'no-undef': 1, strict: 0}}); 116 | 117 | lintStream.pipe(eslint.failAfterError()) 118 | .on('error', done) 119 | .on('finish', done); 120 | 121 | lintStream.end(new File({ 122 | path: 'test/fixtures/invalid.js', 123 | contents: Buffer.from('x = 0;') 124 | })); 125 | }); 126 | 127 | it('should handle ESLint reports without messages', done => { 128 | const file = new File({ 129 | path: 'test/fixtures/invalid.js', 130 | contents: Buffer.from('#invalid!syntax}') 131 | }); 132 | file.eslint = {}; 133 | 134 | eslint.failAfterError() 135 | .on('error', done) 136 | .on('finish', done) 137 | .end(file); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/fixtures/eslintrc-sharable-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@shinnn/node', 4 | rules: { 5 | 'no-redeclare': 'off', 6 | 'no-use-before-define': 'off' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/format.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 'use strict'; 3 | 4 | const File = require('vinyl'); 5 | const stream = require('stream'); 6 | const should = require('should'); 7 | const eslint = require('..'); 8 | 9 | require('mocha'); 10 | 11 | function getFiles() { 12 | return [ 13 | new File({ 14 | path: 'test/fixtures', 15 | contents: null, 16 | isDirectory: true 17 | }), 18 | new File({ 19 | path: 'test/fixtures/use-strict.js', 20 | contents: Buffer.from('(function () {\n\n\tvoid 0;\n\n}());\n\n') 21 | }), 22 | new File({ 23 | path: 'test/fixtures/undeclared.js', 24 | contents: Buffer.from('(function () {\n\t"use strict";\n\n\tx = 0;\n\n}());\n') 25 | }), 26 | new File({ 27 | path: 'test/fixtures/passing.js', 28 | contents: Buffer.from('(function () {\n\n\t"use strict";\n\n}());\n') 29 | }) 30 | ]; 31 | } 32 | 33 | describe('gulp-eslint format', () => { 34 | let formatCount; 35 | let writeCount; 36 | 37 | /** 38 | * Custom ESLint formatted result writer for counting write attempts 39 | * rather than writing to the console. 40 | * 41 | * @param {String} message - a message to count as written 42 | */ 43 | function outputWriter(message) { 44 | should.exist(message); 45 | message.should.match(/^\d+ messages$/); 46 | writeCount++; 47 | } 48 | 49 | /** 50 | * Custom ESLint formatted result writer that will throw an exception 51 | * 52 | * @throws Error Always thrown to test error handling in writers 53 | * @param {String} message - a message to trigger an error 54 | */ 55 | function failWriter(message) { 56 | const error = new Error('Writer Test Error' + (message ? ': ' + message : '')); 57 | error.name = 'TestError'; 58 | throw error; 59 | } 60 | 61 | describe('format all results', () => { 62 | /** 63 | * Custom ESLint result formatter for counting format passes and 64 | * returning a expected formatted result message. 65 | * 66 | * @param {Array} results - ESLint results 67 | * @param {Object} config - format config 68 | * @returns {String} formatted results 69 | */ 70 | function formatResults(results, config) { 71 | should.exist(config); 72 | should.exist(results); 73 | results.should.be.instanceof(Array).with.a.lengthOf(3); 74 | formatCount++; 75 | 76 | const messageCount = results.reduce((sum, result) => { 77 | return sum + result.messages.length; 78 | }, 0); 79 | 80 | return messageCount + ' messages'; 81 | } 82 | 83 | beforeEach(() => { 84 | formatCount = 0; 85 | writeCount = 0; 86 | }); 87 | 88 | it('should format all ESLint results at once', done => { 89 | const files = getFiles(); 90 | 91 | const lintStream = eslint({useEslintrc: false, rules: {'strict': 2}}); 92 | lintStream.on('error', done); 93 | 94 | const formatStream = eslint.format(formatResults, outputWriter); 95 | 96 | formatStream 97 | .on('error', done) 98 | .on('finish', () => { 99 | formatCount.should.equal(1); 100 | writeCount.should.equal(1); 101 | done(); 102 | }); 103 | 104 | should.exist(lintStream.pipe); 105 | lintStream.pipe(formatStream); 106 | 107 | files.forEach(function(file) { 108 | lintStream.write(file); 109 | }); 110 | lintStream.end(); 111 | }); 112 | 113 | it('should not attempt to format when no linting results are found', done => { 114 | const files = getFiles(); 115 | 116 | const passthruStream = new stream.PassThrough({objectMode: true}) 117 | .on('error', done); 118 | 119 | const formatStream = eslint.format(formatResults, outputWriter); 120 | 121 | formatStream 122 | .on('error', done) 123 | .on('finish', () => { 124 | formatCount.should.equal(0); 125 | writeCount.should.equal(0); 126 | done(); 127 | }); 128 | 129 | should.exist(passthruStream.pipe); 130 | passthruStream.pipe(formatStream); 131 | 132 | files.forEach(function(file) { 133 | passthruStream.write(file); 134 | }); 135 | passthruStream.end(); 136 | }); 137 | 138 | }); 139 | 140 | describe('format each result', () => { 141 | 142 | function formatResult(results, config) { 143 | should.exist(config); 144 | should.exist(results); 145 | results.should.be.instanceof(Array).with.a.lengthOf(1); 146 | formatCount++; 147 | 148 | return `${results.reduce((sum, result) => sum + result.messages.length, 0)} messages`; 149 | } 150 | 151 | it('should format individual ESLint results', done => { 152 | formatCount = 0; 153 | writeCount = 0; 154 | 155 | const files = getFiles(); 156 | 157 | const lintStream = eslint({useEslintrc: false, rules: {'strict': 2}}) 158 | .on('error', done); 159 | 160 | const formatStream = eslint.formatEach(formatResult, outputWriter) 161 | .on('error', done) 162 | .on('finish', function() { 163 | // the stream should not have emitted an error 164 | this._writableState.errorEmitted.should.equal(false); 165 | 166 | const fileCount = files.length - 1;// remove directory 167 | formatCount.should.equal(fileCount); 168 | writeCount.should.equal(fileCount); 169 | done(); 170 | }); 171 | 172 | should.exist(lintStream.pipe); 173 | lintStream.pipe(formatStream); 174 | 175 | files.forEach(file => lintStream.write(file)); 176 | lintStream.end(); 177 | }); 178 | 179 | it('should catch and wrap format writer errors in a PluginError', done => { 180 | formatCount = 0; 181 | writeCount = 0; 182 | 183 | const files = getFiles(); 184 | 185 | const lintStream = eslint({useEslintrc: false, rules: {'strict': 2}}) 186 | .on('error', done); 187 | 188 | const formatStream = eslint.formatEach(formatResult, failWriter); 189 | 190 | formatStream 191 | .on('error', err => { 192 | should.exists(err); 193 | err.message.should.equal('Writer Test Error: 1 messages'); 194 | err.name.should.equal('TestError'); 195 | err.plugin.should.equal('gulp-eslint'); 196 | done(); 197 | }) 198 | .on('finish', () => { 199 | done(new Error('Expected PluginError to fail stream')); 200 | }); 201 | 202 | should.exist(lintStream.pipe); 203 | lintStream.pipe(formatStream); 204 | 205 | files.forEach(file => lintStream.write(file)); 206 | lintStream.end(); 207 | }); 208 | 209 | }); 210 | 211 | }); 212 | -------------------------------------------------------------------------------- /test/linting.js: -------------------------------------------------------------------------------- 1 | /* global describe, it*/ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const eslint = require('..'); 6 | const File = require('vinyl'); 7 | const stringToStream = require('from2-string'); 8 | const should = require('should'); 9 | 10 | require('mocha'); 11 | 12 | describe('gulp-eslint plugin', () => { 13 | it('should configure an alternate parser', done => { 14 | eslint({ 15 | parser: 'babel-eslint', 16 | useEslintrc: false, 17 | rules: {'prefer-template': 'error'} 18 | }) 19 | .on('error', done) 20 | .on('data', file => { 21 | should.exist(file); 22 | should.exist(file.contents); 23 | should.exist(file.eslint); 24 | file.eslint.should.have.property('filePath', path.resolve('test/fixtures/stage0-class-property.js')); 25 | 26 | file.eslint.messages 27 | .should.be.instanceof(Array) 28 | .and.have.lengthOf(1); 29 | 30 | file.eslint.messages[0] 31 | .should.have.properties('message', 'line', 'column') 32 | .and.have.property('ruleId', 'prefer-template'); 33 | 34 | done(); 35 | }) 36 | .end(new File({ 37 | path: 'test/fixtures/stage0-class-property.js', 38 | contents: Buffer.from('class MyClass {prop = a + "b" + c;}') 39 | })); 40 | }); 41 | 42 | it('should support sharable config', done => { 43 | eslint(path.resolve(__dirname, 'fixtures', 'eslintrc-sharable-config.js')) 44 | .on('error', done) 45 | .on('data', file => { 46 | should.exist(file); 47 | should.exist(file.contents); 48 | should.exist(file.eslint); 49 | 50 | file.eslint.messages 51 | .should.be.instanceof(Array) 52 | .and.have.lengthOf(1); 53 | 54 | file.eslint.messages[0] 55 | .should.have.properties('message', 'line', 'column') 56 | .and.have.property('ruleId', 'eol-last'); 57 | 58 | done(); 59 | }) 60 | .end(new File({ 61 | path: 'test/fixtures/no-newline.js', 62 | contents: Buffer.from('console.log(\'Hi\');') 63 | })); 64 | }); 65 | 66 | it('should produce expected message via buffer', done => { 67 | eslint({useEslintrc: false, rules: {strict: [2, 'global']}}) 68 | .on('error', done) 69 | .on('data', file => { 70 | should.exist(file); 71 | should.exist(file.contents); 72 | should.exist(file.eslint); 73 | file.eslint.should.have.property('filePath', path.resolve('test/fixtures/use-strict.js')); 74 | 75 | file.eslint.messages 76 | .should.be.instanceof(Array) 77 | .and.have.lengthOf(1); 78 | 79 | file.eslint.messages[0] 80 | .should.have.properties('message', 'line', 'column') 81 | .and.have.property('ruleId', 'strict'); 82 | 83 | done(); 84 | }) 85 | .end(new File({ 86 | path: 'test/fixtures/use-strict.js', 87 | contents: Buffer.from('var x = 1;') 88 | })); 89 | }); 90 | 91 | it('should ignore files with null content', done => { 92 | eslint({useEslintrc: false, rules: {'strict': 2}}) 93 | .on('error', done) 94 | .on('data', file => { 95 | should.exist(file); 96 | should.not.exist(file.contents); 97 | should.not.exist(file.eslint); 98 | done(); 99 | }) 100 | .end(new File({ 101 | path: 'test/fixtures', 102 | isDirectory: true 103 | })); 104 | }); 105 | 106 | it('should emit an error when it takes a steam content', done => { 107 | eslint({useEslintrc: false, rules: {'strict': 'error'}}) 108 | .on('error', err => { 109 | err.plugin.should.equal('gulp-eslint'); 110 | err.message.should.equal('gulp-eslint doesn\'t support vinyl files with Stream contents.'); 111 | done(); 112 | }) 113 | .end(new File({ 114 | path: 'test/fixtures/stream.js', 115 | contents: stringToStream('') 116 | })); 117 | }); 118 | 119 | it('should emit an error when it fails to load a plugin', done => { 120 | const pluginName = 'this-is-unknown-plugin'; 121 | eslint({plugins: [pluginName]}) 122 | .on('error', err => { 123 | err.plugin.should.equal('gulp-eslint'); 124 | // Remove stack trace from error message as it's machine-dependent 125 | const message = err.message.split('\n')[0]; 126 | message.should.equal(`Failed to load plugin '${pluginName}' declared in 'CLIOptions': Cannot find module 'eslint-plugin-${ 127 | pluginName 128 | }'`); 129 | 130 | done(); 131 | }) 132 | .end(new File({ 133 | path: 'test/fixtures/file.js', 134 | contents: Buffer.from('') 135 | })); 136 | }); 137 | 138 | describe('"warnFileIgnored" option', () => { 139 | 140 | it('when true, should warn when a file is ignored by .eslintignore', done => { 141 | eslint({useEslintrc: false, warnFileIgnored: true}) 142 | .on('error', done) 143 | .on('data', file => { 144 | should.exist(file); 145 | should.exist(file.eslint); 146 | file.eslint.messages.should.be.instanceof(Array).and.have.lengthOf(1); 147 | file.eslint.messages[0] 148 | .should.have.property('message', 'File ignored because of .eslintignore file'); 149 | file.eslint.errorCount.should.equal(0); 150 | file.eslint.warningCount.should.equal(1); 151 | done(); 152 | }) 153 | .end(new File({ 154 | path: 'test/fixtures/ignored.js', 155 | contents: Buffer.from('(function () {ignore = abc;}});') 156 | })); 157 | }); 158 | 159 | it('when true, should warn when a "node_modules" file is ignored', done => { 160 | eslint({useEslintrc: false, warnFileIgnored: true}) 161 | .on('error', done) 162 | .on('data', file => { 163 | should.exist(file); 164 | should.exist(file.eslint); 165 | file.eslint.messages.should.be.instanceof(Array).and.have.lengthOf(1); 166 | file.eslint.messages[0].should.have.property('message', 167 | 'File ignored because it has a node_modules/** path'); 168 | file.eslint.errorCount.should.equal(0); 169 | file.eslint.warningCount.should.equal(1); 170 | done(); 171 | }) 172 | .end(new File({ 173 | path: 'node_modules/test/index.js', 174 | contents: Buffer.from('(function () {ignore = abc;}});') 175 | })); 176 | }); 177 | 178 | it('when not true, should silently ignore files', done => { 179 | eslint({useEslintrc: false, warnFileIgnored: false}) 180 | .on('error', done) 181 | .on('data', file => { 182 | should.exist(file); 183 | should.not.exist(file.eslint); 184 | done(); 185 | }) 186 | .end(new File({ 187 | path: 'test/fixtures/ignored.js', 188 | contents: Buffer.from('(function () {ignore = abc;}});') 189 | })); 190 | }); 191 | 192 | }); 193 | 194 | describe('"quiet" option', () => { 195 | 196 | it('when true, should remove warnings', done => { 197 | eslint({quiet: true, useEslintrc: false, rules: {'no-undef': 1, 'strict': 2}}) 198 | .on('data', file => { 199 | should.exist(file); 200 | should.exist(file.eslint); 201 | file.eslint.messages.should.be.instanceof(Array).and.have.lengthOf(1); 202 | file.eslint.errorCount.should.equal(1); 203 | file.eslint.warningCount.should.equal(0); 204 | done(); 205 | }) 206 | .end(new File({ 207 | path: 'test/fixtures/invalid.js', 208 | contents: Buffer.from('function z() { x = 0; }') 209 | })); 210 | }); 211 | 212 | it('when a function, should filter messages', done => { 213 | function warningsOnly(message) { 214 | return message.severity === 1; 215 | } 216 | eslint({quiet: warningsOnly, useEslintrc: false, rules: {'no-undef': 1, 'strict': 2}}) 217 | .on('data', file => { 218 | should.exist(file); 219 | should.exist(file.eslint); 220 | file.eslint.messages.should.be.instanceof(Array).and.have.lengthOf(1); 221 | file.eslint.errorCount.should.equal(0); 222 | file.eslint.warningCount.should.equal(1); 223 | done(); 224 | }) 225 | .end(new File({ 226 | path: 'test/fixtures/invalid.js', 227 | contents: Buffer.from('function z() { x = 0; }') 228 | })); 229 | }); 230 | 231 | }); 232 | 233 | describe('"fix" option', () => { 234 | 235 | it('when true, should update buffered contents', done => { 236 | eslint({fix: true, useEslintrc: false, rules: {'no-trailing-spaces': 2}}) 237 | .on('error', done) 238 | .on('data', (file) => { 239 | should.exist(file); 240 | should.exist(file.eslint); 241 | file.eslint.messages.should.be.instanceof(Array).and.have.lengthOf(0); 242 | file.eslint.errorCount.should.equal(0); 243 | file.eslint.warningCount.should.equal(0); 244 | file.eslint.output.should.equal('var x = 0;'); 245 | file.contents.toString().should.equal('var x = 0;'); 246 | done(); 247 | }) 248 | .end(new File({ 249 | path: 'test/fixtures/fixable.js', 250 | contents: Buffer.from('var x = 0; ') 251 | })); 252 | }); 253 | }); 254 | 255 | }); 256 | -------------------------------------------------------------------------------- /test/result.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 'use strict'; 3 | 4 | const File = require('vinyl'); 5 | const PassThrough = require('stream').PassThrough; 6 | const should = require('should'); 7 | const eslint = require('..'); 8 | 9 | require('mocha'); 10 | 11 | describe('gulp-eslint result', () => { 12 | it('should provide an ESLint result', done => { 13 | let resultCount = 0; 14 | const lintStream = eslint({ 15 | useEslintrc: false, 16 | rules: { 17 | 'no-undef': 2, 18 | 'strict': [1, 'global'] 19 | } 20 | }); 21 | 22 | lintStream 23 | .pipe(eslint.result(result => { 24 | should.exists(result); 25 | result.messages.should.be.instanceof(Array).with.a.lengthOf(2); 26 | result.errorCount.should.equal(1); 27 | result.warningCount.should.equal(1); 28 | resultCount++; 29 | })) 30 | .on('finish', () => { 31 | resultCount.should.equal(3); 32 | done(); 33 | }); 34 | 35 | lintStream.write(new File({ 36 | path: 'test/fixtures/invalid-1.js', 37 | contents: Buffer.from('x = 1;') 38 | })); 39 | 40 | lintStream.write(new File({ 41 | path: 'test/fixtures/invalid-2.js', 42 | contents: Buffer.from('x = 2;') 43 | })); 44 | 45 | lintStream.write(new File({ 46 | path: 'test/fixtures/invalid-3.js', 47 | contents: Buffer.from('x = 3;') 48 | })); 49 | 50 | lintStream.end(); 51 | }); 52 | 53 | it('should catch thrown errors', done => { 54 | const file = new File({ 55 | path: 'test/fixtures/invalid.js', 56 | contents: Buffer.from('#invalid!syntax}') 57 | }); 58 | file.eslint = {}; 59 | 60 | function finished() { 61 | done(new Error('Unexpected Finish')); 62 | } 63 | 64 | eslint.result(() => { 65 | throw new Error('Expected Error'); 66 | }) 67 | .on('error', function(error) { 68 | this.removeListener('finish', finished); 69 | should.exists(error); 70 | error.message.should.equal('Expected Error'); 71 | error.name.should.equal('Error'); 72 | error.plugin.should.equal('gulp-eslint'); 73 | done(); 74 | }) 75 | .on('finish', finished) 76 | .end(file); 77 | }); 78 | 79 | it('should catch thrown null', done => { 80 | const file = new File({ 81 | path: 'test/fixtures/invalid.js', 82 | contents: Buffer.from('#invalid!syntax}') 83 | }); 84 | file.eslint = {}; 85 | 86 | function finished() { 87 | done(new Error('Unexpected Finish')); 88 | } 89 | 90 | eslint.result(() => { 91 | throw null; 92 | }) 93 | .on('error', function(error) { 94 | this.removeListener('finish', finished); 95 | should.exists(error); 96 | error.message.should.equal('Unknown Error'); 97 | error.name.should.equal('Error'); 98 | error.plugin.should.equal('gulp-eslint'); 99 | done(); 100 | }) 101 | .on('finish', finished) 102 | .end(file); 103 | }); 104 | 105 | it('should throw an error if not provided a function argument', () => { 106 | 107 | try { 108 | eslint.result(); 109 | } catch (error) { 110 | should.exists(error); 111 | should.exists(error.message); 112 | error.message.should.equal('Expected callable argument'); 113 | return; 114 | } 115 | 116 | throw new Error('Expected exception to be thrown'); 117 | 118 | }); 119 | 120 | it('should ignore files without an ESLint result', done => { 121 | 122 | const file = new File({ 123 | path: 'test/fixtures/invalid.js', 124 | contents: Buffer.from('#invalid!syntax}') 125 | }); 126 | 127 | eslint.result(() => { 128 | throw new Error('Expected no call'); 129 | }) 130 | .on('error', function(error) { 131 | this.removeListener('finish', done); 132 | done(error); 133 | }) 134 | .on('finish', done) 135 | .end(file); 136 | }); 137 | 138 | it('should support an async result handler', done => { 139 | let asyncComplete = false; 140 | const file = new File({ 141 | path: 'test/fixtures/invalid.js', 142 | contents: Buffer.from('#invalid!syntax}') 143 | }); 144 | const resultStub = {}; 145 | file.eslint = resultStub; 146 | 147 | function ended() { 148 | asyncComplete.should.equal(true); 149 | done(); 150 | } 151 | 152 | const resultStream = eslint.result((result, callback) => { 153 | should.exists(result); 154 | result.should.equal(resultStub); 155 | 156 | (typeof callback).should.equal('function'); 157 | 158 | setTimeout(() => { 159 | asyncComplete = true; 160 | callback(); 161 | }, 10); 162 | }) 163 | .on('error', function(error) { 164 | this.removeListener('end', ended); 165 | done(error); 166 | }) 167 | .on('end', ended); 168 | 169 | // drain result into pass-through stream 170 | resultStream.pipe(new PassThrough({objectMode: true})); 171 | 172 | resultStream.end(file); 173 | 174 | }); 175 | 176 | }); 177 | 178 | describe('gulp-eslint results', () => { 179 | 180 | it('should provide ESLint results', done => { 181 | let resultsCalled = false; 182 | const lintStream = eslint({ 183 | useEslintrc: false, 184 | rules: { 185 | 'no-undef': 2, 186 | 'strict': [1, 'global'] 187 | } 188 | }); 189 | 190 | lintStream 191 | .pipe(eslint.results(results => { 192 | should.exists(results); 193 | results.should.be.instanceof(Array).with.a.lengthOf(3); 194 | results.errorCount.should.equal(3); 195 | results.warningCount.should.equal(3); 196 | resultsCalled = true; 197 | })) 198 | .on('finish', () => { 199 | resultsCalled.should.equal(true); 200 | done(); 201 | }); 202 | 203 | lintStream.write(new File({ 204 | path: 'test/fixtures/invalid-1.js', 205 | contents: Buffer.from('x = 1;') 206 | })); 207 | 208 | lintStream.write(new File({ 209 | path: 'test/fixtures/invalid-2.js', 210 | contents: Buffer.from('x = 2;') 211 | })); 212 | 213 | lintStream.write(new File({ 214 | path: 'test/fixtures/invalid-3.js', 215 | contents: Buffer.from('x = 3;') 216 | })); 217 | 218 | lintStream.end(); 219 | }); 220 | 221 | it('should catch thrown errors', done => { 222 | const file = new File({ 223 | path: 'test/fixtures/invalid.js', 224 | contents: Buffer.from('#invalid!syntax}') 225 | }); 226 | file.eslint = {}; 227 | 228 | function finished() { 229 | done(new Error('Unexpected Finish')); 230 | } 231 | 232 | eslint.results(() => { 233 | throw new Error('Expected Error'); 234 | }) 235 | .on('error', function(error) { 236 | this.removeListener('finish', finished); 237 | should.exists(error); 238 | error.message.should.equal('Expected Error'); 239 | error.name.should.equal('Error'); 240 | error.plugin.should.equal('gulp-eslint'); 241 | done(); 242 | }) 243 | .on('finish', finished) 244 | .end(file); 245 | }); 246 | 247 | it('should throw an error if not provided a function argument', () => { 248 | 249 | try { 250 | eslint.results(); 251 | } catch (error) { 252 | should.exists(error); 253 | should.exists(error.message); 254 | error.message.should.equal('Expected callable argument'); 255 | return; 256 | } 257 | 258 | throw new Error('Expected exception to be thrown'); 259 | 260 | }); 261 | 262 | it('should ignore files without an ESLint result', done => { 263 | let resultsCalled = false; 264 | const file = new File({ 265 | path: 'test/fixtures/invalid.js', 266 | contents: Buffer.from('#invalid!syntax}') 267 | }); 268 | 269 | function finished() { 270 | resultsCalled.should.equal(true); 271 | done(); 272 | } 273 | 274 | eslint.results(results => { 275 | should.exists(results); 276 | results.should.be.instanceof(Array).with.a.lengthOf(0); 277 | resultsCalled = true; 278 | }) 279 | .on('error', function(error) { 280 | this.removeListener('finish', finished); 281 | done(error); 282 | }) 283 | .on('finish', finished) 284 | .end(file); 285 | }); 286 | 287 | it('should support an async results handler', done => { 288 | let asyncComplete = false; 289 | const file = new File({ 290 | path: 'test/fixtures/invalid.js', 291 | contents: Buffer.from('#invalid!syntax}') 292 | }); 293 | const resultStub = {}; 294 | file.eslint = resultStub; 295 | 296 | function ended() { 297 | asyncComplete.should.equal(true); 298 | done(); 299 | } 300 | 301 | const resultStream = eslint.results((results, callback) => { 302 | should.exists(results); 303 | results.should.be.instanceof(Array).with.a.lengthOf(1); 304 | 305 | const result = results[0]; 306 | result.should.equal(resultStub); 307 | 308 | (typeof callback).should.equal('function'); 309 | 310 | setTimeout(() => { 311 | asyncComplete = true; 312 | callback(); 313 | }, 10); 314 | }) 315 | .on('error', function(error) { 316 | this.removeListener('end', ended); 317 | done(error); 318 | }) 319 | .on('end', ended); 320 | 321 | // drain result into pass-through stream 322 | resultStream.pipe(new PassThrough({objectMode: true})); 323 | 324 | resultStream.end(file); 325 | 326 | }); 327 | 328 | }); 329 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, afterEach */ 2 | 'use strict'; 3 | 4 | const File = require('vinyl'); 5 | const stream = require('stream'); 6 | const should = require('should'); 7 | const util = require('../util'); 8 | 9 | require('mocha'); 10 | 11 | describe('utility methods', () => { 12 | describe('transform', () => { 13 | it('should handle files in a stream', done => { 14 | let passedFile = false; 15 | const streamFile = new File({ 16 | path: 'test/fixtures/invalid.js', 17 | contents: Buffer.from('x = 1;') 18 | }); 19 | const testStream = util.transform((file, enc, cb) => { 20 | should.exist(file); 21 | should.exist(cb); 22 | passedFile = (streamFile === file); 23 | cb(); 24 | }) 25 | .on('error', done) 26 | .on('finish', () => { 27 | passedFile.should.equal(true); 28 | done(); 29 | }); 30 | 31 | testStream.end(streamFile); 32 | }); 33 | 34 | it('should flush when stream is ending', done => { 35 | let count = 0; 36 | let finalCount = 0; 37 | const files = [ 38 | new File({ 39 | path: 'test/fixtures/invalid.js', 40 | contents: Buffer.from('x = 1;') 41 | }), 42 | new File({ 43 | path: 'test/fixtures/undeclared.js', 44 | contents: Buffer.from('x = 0;') 45 | }) 46 | ]; 47 | const testStream = util.transform((file, enc, cb) => { 48 | should.exist(file); 49 | should.exist(cb); 50 | count += 1; 51 | cb(); 52 | }, cb => { 53 | should.exist(cb); 54 | count.should.equal(files.length); 55 | testStream._writableState.ending.should.equal(true); 56 | finalCount = count; 57 | cb(); 58 | }) 59 | .on('error', done) 60 | .on('finish', () => { 61 | finalCount.should.equal(files.length); 62 | done(); 63 | }); 64 | 65 | files.forEach(file => testStream.write(file)); 66 | 67 | testStream.end(); 68 | 69 | }); 70 | 71 | }); 72 | 73 | describe('createIgnoreResult', () => { 74 | it('should create a warning that the file is ignored by ".eslintignore"', () => { 75 | const file = new File({ 76 | path: 'test/fixtures/ignored.js', 77 | contents: Buffer.from('') 78 | }); 79 | const result = util.createIgnoreResult(file); 80 | should.exist(result); 81 | result.filePath.should.equal(file.path); 82 | result.errorCount.should.equal(0); 83 | result.warningCount.should.equal(1); 84 | result.messages.should.be.instanceof(Array).and.have.lengthOf(1); 85 | result.messages[0].message.should.equal('File ignored because of .eslintignore file'); 86 | 87 | }); 88 | 89 | it('should create a warning for paths that include "node_modules"', () => { 90 | const file = new File({ 91 | path: 'node_modules/test/index.js', 92 | contents: Buffer.from('') 93 | }); 94 | const result = util.createIgnoreResult(file); 95 | should.exist(result); 96 | result.filePath.should.equal(file.path); 97 | result.errorCount.should.equal(0); 98 | result.warningCount.should.equal(1); 99 | result.messages.should.be.instanceof(Array).and.have.lengthOf(1); 100 | result.messages[0].message.should.equal( 101 | 'File ignored because it has a node_modules/** path' 102 | ); 103 | 104 | }); 105 | 106 | }); 107 | 108 | describe('migrateOptions', () => { 109 | it('should migrate a string config value to "configPath"', () => { 110 | const options = util.migrateOptions('Config/Path'); 111 | should.exist(options.configFile); 112 | options.configFile.should.equal('Config/Path'); 113 | }); 114 | }); 115 | 116 | describe('isErrorMessage', () => { 117 | 118 | it('should determine severity a "fatal" message flag', () => { 119 | const errorMessage = { 120 | fatal: true, 121 | severity: 0 122 | }; 123 | const isError = util.isErrorMessage(errorMessage); 124 | isError.should.equal(true); 125 | 126 | }); 127 | 128 | it('should determine severity from an config array', () => { 129 | const errorMessage = { 130 | severity: [2, 1] 131 | }; 132 | const isError = util.isErrorMessage(errorMessage); 133 | isError.should.equal(true); 134 | 135 | }); 136 | 137 | }); 138 | 139 | describe('filterResult', () => { 140 | 141 | const result = { 142 | filePath: 'test/fixtures/invalid.js', 143 | messages: [{ 144 | ruleId: 'error', 145 | severity: 2, 146 | message: 'This is an error.', 147 | line: 1, 148 | column: 1, 149 | nodeType: 'FunctionDeclaration', 150 | source: 'function a() { x = 0; }' 151 | },{ 152 | ruleId: 'warning', 153 | severity: 1, 154 | message: 'This is a warning.', 155 | line: 1, 156 | column: 1, 157 | nodeType: 'FunctionDeclaration', 158 | source: 'function a() { x = 0; }' 159 | }], 160 | errorCount: 1, 161 | warningCount: 1 162 | }; 163 | 164 | it('should filter messages', () => { 165 | function warningsOnly(message) { 166 | return message.severity === 1; 167 | } 168 | const quietResult = util.filterResult(result, warningsOnly); 169 | quietResult.filePath.should.equal('test/fixtures/invalid.js'); 170 | quietResult.messages.should.be.instanceof(Array).and.have.lengthOf(1); 171 | quietResult.errorCount.should.equal(0); 172 | quietResult.warningCount.should.equal(1); 173 | }); 174 | 175 | it('should remove warning messages', () => { 176 | const quietResult = util.filterResult(result, true); 177 | quietResult.filePath.should.equal('test/fixtures/invalid.js'); 178 | quietResult.messages.should.be.instanceof(Array).and.have.lengthOf(1); 179 | quietResult.errorCount.should.equal(1); 180 | quietResult.warningCount.should.equal(0); 181 | }); 182 | 183 | }); 184 | 185 | describe('resolveFormatter', () => { 186 | 187 | it('should default to the "stylish" formatter', () => { 188 | 189 | const formatter = util.resolveFormatter(); 190 | formatter.should.equal(require('eslint/lib/cli-engine/formatters/stylish')); 191 | 192 | }); 193 | 194 | it('should resolve a formatter', () => { 195 | 196 | const formatter = util.resolveFormatter('tap'); 197 | formatter.should.equal(require('eslint/lib/cli-engine/formatters/tap')); 198 | 199 | }); 200 | 201 | it('should throw an error if a formatter cannot be resolved', () => { 202 | 203 | function resolveMissingFormatter() { 204 | util.resolveFormatter('missing-formatter'); 205 | } 206 | resolveMissingFormatter.should.throw(Error, { 207 | message: /There was a problem loading formatter/ 208 | }); 209 | 210 | }); 211 | 212 | }); 213 | 214 | describe('resolveWritable', () => { 215 | 216 | it('should default to fancyLog', () => { 217 | 218 | const write = util.resolveWritable(); 219 | write.should.equal(require('fancy-log')); 220 | 221 | }); 222 | 223 | it('should write to a (writable) stream', function(done) { 224 | 225 | let written = false; 226 | const writable = new stream.Writable({objectMode: true}); 227 | const testValue = 'Formatted Output'; 228 | const write = util.resolveWritable(writable); 229 | 230 | writable._write = function writeChunk(chunk, encoding, cb) { 231 | should.exist(chunk); 232 | chunk.should.equal(testValue); 233 | written = true; 234 | cb(); 235 | }; 236 | 237 | writable 238 | .on('error', done) 239 | .on('finish', () => { 240 | written.should.equal(true); 241 | done(); 242 | }); 243 | write(testValue); 244 | writable.end(); 245 | 246 | }); 247 | 248 | }); 249 | 250 | describe('writeResults', () => { 251 | 252 | const testConfig = {}, 253 | testResult = { 254 | config: testConfig 255 | }, 256 | testResults = [testResult]; 257 | 258 | it('should pass the value returned from the formatter to the writer', () => { 259 | 260 | const testValue = {}; 261 | 262 | function testFormatter(results, config) { 263 | should.exist(results); 264 | results.should.equal(testResults); 265 | should.exist(config); 266 | config.should.equal(testConfig); 267 | 268 | return testValue; 269 | } 270 | 271 | function testWriter(value) { 272 | should.exist(value); 273 | value.should.equal(testValue); 274 | } 275 | 276 | util.writeResults(testResults, testFormatter, testWriter); 277 | 278 | }); 279 | 280 | it('should not write an empty or missing value', () => { 281 | 282 | function testFormatter(results, config) { 283 | should.exist(results); 284 | results.should.equal(testResults); 285 | should.exist(config); 286 | config.should.equal(testConfig); 287 | 288 | return ''; 289 | } 290 | 291 | function testWriter(value) { 292 | should.not.exist(value); 293 | } 294 | 295 | util.writeResults(testResults, testFormatter, testWriter); 296 | 297 | }); 298 | 299 | it('should default undefined results to an empty array', () => { 300 | 301 | function testFormatter(results, config) { 302 | should.exist(results); 303 | results.should.be.instanceof(Array).and.have.lengthOf(0); 304 | should.exist(config); 305 | 306 | return results.length + ' results'; 307 | } 308 | 309 | function testWriter(value) { 310 | should.exist(value); 311 | value.should.equal('0 results'); 312 | } 313 | 314 | util.writeResults(null, testFormatter, testWriter); 315 | 316 | }); 317 | 318 | }); 319 | 320 | }); 321 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Transform} = require('stream'); 4 | const PluginError = require('plugin-error'); 5 | const fancyLog = require('fancy-log'); 6 | const {CLIEngine} = require('eslint'); 7 | 8 | /** 9 | * Convenience method for creating a transform stream in object mode 10 | * 11 | * @param {Function} transform - An async function that is called for each stream chunk 12 | * @param {Function} [flush] - An async function that is called before closing the stream 13 | * @returns {stream} A transform stream 14 | */ 15 | exports.transform = function(transform, flush) { 16 | if (typeof flush === 'function') { 17 | return new Transform({ 18 | objectMode: true, 19 | transform, 20 | flush 21 | }); 22 | } 23 | 24 | return new Transform({ 25 | objectMode: true, 26 | transform 27 | }); 28 | }; 29 | 30 | /** 31 | * Mimic the CLIEngine's createIgnoreResult function, 32 | * only without the ESLint CLI reference. 33 | * 34 | * @param {Object} file - file with a "path" property 35 | * @returns {Object} An ESLint report with an ignore warning 36 | */ 37 | exports.createIgnoreResult = file => { 38 | return { 39 | filePath: file.path, 40 | messages: [{ 41 | fatal: false, 42 | severity: 1, 43 | message: file.path.includes('node_modules/') ? 44 | 'File ignored because it has a node_modules/** path' : 45 | 'File ignored because of .eslintignore file' 46 | }], 47 | errorCount: 0, 48 | warningCount: 1 49 | }; 50 | }; 51 | 52 | /** 53 | * Create config helper to merge various config sources 54 | * 55 | * @param {Object} options - options to migrate 56 | * @returns {Object} migrated options 57 | */ 58 | exports.migrateOptions = function migrateOptions(options) { 59 | if (typeof options === 'string') { 60 | // basic config path overload: gulpEslint('path/to/config.json') 61 | options = { 62 | configFile: options 63 | }; 64 | } 65 | 66 | return options; 67 | }; 68 | 69 | /** 70 | * Ensure that callback errors are wrapped in a gulp PluginError 71 | * 72 | * @param {Function} callback - callback to wrap 73 | * @param {Object} [value=] - A value to pass to the callback 74 | * @returns {Function} A callback to call(back) the callback 75 | */ 76 | exports.handleCallback = (callback, value) => { 77 | return err => { 78 | if (err != null && !(err instanceof PluginError)) { 79 | err = new PluginError(err.plugin || 'gulp-eslint', err, { 80 | showStack: (err.showStack !== false) 81 | }); 82 | } 83 | 84 | callback(err, value); 85 | }; 86 | }; 87 | 88 | /** 89 | * Call sync or async action and handle any thrown or async error 90 | * 91 | * @param {Function} action - Result action to call 92 | * @param {(Object|Array)} result - An ESLint result or result list 93 | * @param {Function} done - An callback for when the action is complete 94 | */ 95 | exports.tryResultAction = function(action, result, done) { 96 | try { 97 | if (action.length > 1) { 98 | // async action 99 | action.call(this, result, done); 100 | } else { 101 | // sync action 102 | action.call(this, result); 103 | done(); 104 | } 105 | } catch (error) { 106 | done(error == null ? new Error('Unknown Error') : error); 107 | } 108 | }; 109 | 110 | /** 111 | * Get first message in an ESLint result to meet a condition 112 | * 113 | * @param {Object} result - An ESLint result 114 | * @param {Function} condition - A condition function that is passed a message and returns a boolean 115 | * @returns {Object} The first message to pass the condition or null 116 | */ 117 | exports.firstResultMessage = (result, condition) => { 118 | if (!result.messages) { 119 | return null; 120 | } 121 | 122 | return result.messages.find(condition); 123 | }; 124 | 125 | /** 126 | * Determine if a message is an error 127 | * 128 | * @param {Object} message - an ESLint message 129 | * @returns {Boolean} whether the message is an error message 130 | */ 131 | function isErrorMessage(message) { 132 | const level = message.fatal ? 2 : message.severity; 133 | 134 | if (Array.isArray(level)) { 135 | return level[0] > 1; 136 | } 137 | 138 | return level > 1; 139 | } 140 | exports.isErrorMessage = isErrorMessage; 141 | 142 | /** 143 | * Increment count if message is an error 144 | * 145 | * @param {Number} count - count of errors 146 | * @param {Object} message - an ESLint message 147 | * @returns {Number} The number of errors, message included 148 | */ 149 | function countErrorMessage(count, message) { 150 | return count + Number(isErrorMessage(message)); 151 | } 152 | 153 | /** 154 | * Increment count if message is a warning 155 | * 156 | * @param {Number} count - count of warnings 157 | * @param {Object} message - an ESLint message 158 | * @returns {Number} The number of warnings, message included 159 | */ 160 | function countWarningMessage(count, message) { 161 | return count + Number(message.severity === 1); 162 | } 163 | 164 | /** 165 | * Increment count if message is a fixable error 166 | * 167 | * @param {Number} count - count of errors 168 | * @param {Object} message - an ESLint message 169 | * @returns {Number} The number of errors, message included 170 | */ 171 | function countFixableErrorMessage(count, message) { 172 | return count + Number(isErrorMessage(message) && message.fix !== undefined); 173 | } 174 | 175 | /** 176 | * Increment count if message is a fixable warning 177 | * 178 | * @param {Number} count - count of errors 179 | * @param {Object} message - an ESLint message 180 | * @returns {Number} The number of errors, message included 181 | */ 182 | function countFixableWarningMessage(count, message) { 183 | return count + Number(message.severity === 1 && message.fix !== undefined); 184 | } 185 | 186 | /** 187 | * Filter result messages, update error and warning counts 188 | * 189 | * @param {Object} result - an ESLint result 190 | * @param {Function} [filter=isErrorMessage] - A function that evaluates what messages to keep 191 | * @returns {Object} A filtered ESLint result 192 | */ 193 | exports.filterResult = (result, filter) => { 194 | if (typeof filter !== 'function') { 195 | filter = isErrorMessage; 196 | } 197 | const messages = result.messages.filter(filter, result); 198 | const newResult = { 199 | filePath: result.filePath, 200 | messages: messages, 201 | errorCount: messages.reduce(countErrorMessage, 0), 202 | warningCount: messages.reduce(countWarningMessage, 0), 203 | fixableErrorCount: messages.reduce(countFixableErrorMessage, 0), 204 | fixableWarningCount: messages.reduce(countFixableWarningMessage, 0), 205 | }; 206 | 207 | if (result.output !== undefined) { 208 | newResult.output = result.output; 209 | } else { 210 | newResult.source = result.source; 211 | } 212 | 213 | return newResult; 214 | }; 215 | 216 | /** 217 | * Resolve formatter from unknown type (accepts string or function) 218 | * 219 | * @throws TypeError thrown if unable to resolve the formatter type 220 | * @param {(String|Function)} [formatter=stylish] - A name to resolve as a formatter. If a function is provided, the same function is returned. 221 | * @returns {Function} An ESLint formatter 222 | */ 223 | exports.resolveFormatter = (formatter) => { 224 | // use ESLint to look up formatter references 225 | if (typeof formatter !== 'function') { 226 | // load formatter (module, relative to cwd, ESLint formatter) 227 | formatter = CLIEngine.getFormatter(formatter) || formatter; 228 | } 229 | 230 | return formatter; 231 | }; 232 | 233 | /** 234 | * Resolve writable 235 | * 236 | * @param {(Function|stream)} [writable=fancyLog] - A stream or function to resolve as a format writer 237 | * @returns {Function} A function that writes formatted messages 238 | */ 239 | exports.resolveWritable = (writable) => { 240 | if (!writable) { 241 | writable = fancyLog; 242 | } else if (typeof writable.write === 'function') { 243 | writable = writable.write.bind(writable); 244 | } 245 | return writable; 246 | }; 247 | 248 | /** 249 | * Write formatter results to writable/output 250 | * 251 | * @param {Object[]} results - A list of ESLint results 252 | * @param {Function} formatter - A function used to format ESLint results 253 | * @param {Function} writable - A function used to write formatted ESLint results 254 | */ 255 | exports.writeResults = (results, formatter, writable) => { 256 | if (!results) { 257 | results = []; 258 | } 259 | 260 | const firstResult = results.find(result => result.config); 261 | 262 | const message = formatter(results, firstResult ? firstResult.config : {}); 263 | if (writable && message != null && message !== '') { 264 | writable(message); 265 | } 266 | }; 267 | --------------------------------------------------------------------------------