├── test ├── parserOptions.js ├── index.js ├── .vscode │ └── launch.json └── lib │ └── rules │ ├── no-yield-in-race-spec.js │ ├── no-unhandled-errors-spec.js │ └── yield-effects-spec.js ├── .editorconfig ├── .travis.yml ├── docs └── rules │ ├── no-unhandled-errors.md │ ├── no-yield-in-race.md │ └── yield-effects.md ├── lib ├── utils │ └── effects.js └── rules │ ├── no-unhandled-errors.js │ ├── yield-effects.js │ └── no-yield-in-race.js ├── .gitignore ├── index.js ├── .vscode └── launch.json ├── LICENSE ├── package.json ├── README.md └── .eslintrc /test/parserOptions.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | module.exports = { 4 | ecmaVersion: 6, 5 | sourceType: "module" 6 | } 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var RuleTester = require("eslint").RuleTester 4 | var parserOptions = require("./parserOptions") 5 | 6 | RuleTester.setDefaultConfig({ 7 | parserOptions: parserOptions 8 | }) 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | - node 5 | env: 6 | matrix: 7 | - REDUX_SAGA_VERSION=^0.11.1 8 | - REDUX_SAGA_VERSION=^1.0.0 9 | before_script: 10 | - npm install redux-saga@${REDUX_SAGA_VERSION} 11 | deploy: 12 | on: 13 | branch: master 14 | provider: npm 15 | email: "$NPM_EMAIL" 16 | api_key: "$NPM_TOKEN" 17 | -------------------------------------------------------------------------------- /docs/rules/no-unhandled-errors.md: -------------------------------------------------------------------------------- 1 | # Ensures error handling on sagas 2 | 3 | ![](https://img.shields.io/badge/-recommended-lightgrey.svg "recommended") 4 | 5 | This rule ensures that all `redux-saga` effects are inside a try/catch block for error handling. 6 | 7 | An uncaught error can cause all other sagas waiting to complete to be inadvertedly canceled. 8 | 9 | ```es6 10 | import { call } from "redux-saga" 11 | 12 | // good 13 | function* good() { 14 | try { 15 | yield call(action) 16 | } catch (error) { 17 | yield call(handleError, error) 18 | } 19 | } 20 | 21 | // bad 22 | function* bad() { 23 | call(action) 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /lib/utils/effects.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var miscEffects = ["delay"] 4 | 5 | function knowEffects() { 6 | var other = Object.keys(require("redux-saga")).filter(function(effect) { 7 | return miscEffects.includes(effect) 8 | }) 9 | 10 | var reduxSagaEffects 11 | try { 12 | reduxSagaEffects = require("redux-saga/lib/effects") 13 | } catch (err) { 14 | reduxSagaEffects = require("redux-saga/effects") 15 | } 16 | 17 | return Object.keys(reduxSagaEffects).concat(other) 18 | } 19 | 20 | function isEffectImport(value) { 21 | return /^redux-saga(\/effects)?/.test(value) 22 | } 23 | 24 | module.exports = { 25 | isEffectImport: isEffectImport, 26 | knowEffects: knowEffects 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # vscode 36 | .vscode 37 | -------------------------------------------------------------------------------- /test/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/best-before.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": null, 13 | "runtimeArgs": [ 14 | "--nolazy" 15 | ], 16 | "env": { 17 | "NODE_ENV": "development" 18 | }, 19 | "externalConsole": false, 20 | "sourceMaps": false, 21 | "outDir": null 22 | }, 23 | { 24 | "name": "Attach", 25 | "type": "node", 26 | "request": "attach", 27 | "port": 5858, 28 | "sourceMaps": false, 29 | "outDir": null, 30 | "localRoot": "${workspaceRoot}", 31 | "remoteRoot": null 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var rules = { 4 | "no-yield-in-race": require("./lib/rules/no-yield-in-race"), 5 | "yield-effects": require("./lib/rules/yield-effects"), 6 | "no-unhandled-errors": require("./lib/rules/no-unhandled-errors") 7 | } 8 | 9 | var allRules = Object.keys(rules).reduce(function(result, name) { 10 | result["redux-saga/" + name] = 2 11 | return result 12 | }, {}) 13 | 14 | module.exports = { 15 | rules: rules, 16 | configs: { 17 | recommended: { 18 | plugins: ["redux-saga"], 19 | rules: { 20 | "redux-saga/no-yield-in-race": 2, 21 | "redux-saga/yield-effects": 2, 22 | "redux-saga/no-unhandled-errors": 2 23 | } 24 | }, 25 | all: { 26 | plugins: ["redux-saga"], 27 | rules: allRules 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${file}", 9 | "stopOnEntry": false, 10 | "args": [ 11 | ], 12 | "cwd": "${workspaceRoot}", 13 | "preLaunchTask": null, 14 | "runtimeExecutable": null, 15 | "runtimeArgs": [ 16 | "--nolazy" 17 | ], 18 | "env": { 19 | "NODE_ENV": "development" 20 | }, 21 | "externalConsole": false, 22 | "sourceMaps": false, 23 | "outDir": null 24 | }, 25 | { 26 | "name": "Attach", 27 | "type": "node", 28 | "request": "attach", 29 | "port": 5858, 30 | "address": "localhost", 31 | "restart": false, 32 | "sourceMaps": false, 33 | "outDir": null, 34 | "localRoot": "${workspaceRoot}", 35 | "remoteRoot": null 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /docs/rules/no-yield-in-race.md: -------------------------------------------------------------------------------- 1 | # Prevent usage of yield in race entries (no-yield-in-race) 2 | 3 | ![](https://img.shields.io/badge/-recommended-lightgrey.svg "recommended") ![fixable](https://img.shields.io/badge/-fixable-green.svg "The `--fix` option on the command line automatically fixes problems reported by this rule.") 4 | 5 | ```es6 6 | import { race, call } from "redux-saga" 7 | 8 | // Good 9 | function* good() { 10 | yield race({ 11 | posts: call(fetchApis) 12 | }) 13 | } 14 | 15 | function* good() { 16 | yield race({ 17 | watchers: [call(watcher1), call(watcher2)] 18 | }) 19 | } 20 | 21 | // Bad 22 | function* bad() { 23 | yield race({ posts: yield call(fetchApis) }) 24 | } 25 | 26 | function* bad() { 27 | yield race({ 28 | watchers: yield [call(watcher1), call(watcher2)] 29 | }) 30 | } 31 | 32 | ``` 33 | 34 | The `--fix` option on the command line automatically fixes problems reported by this rule. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Philipp Kursawe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/rules/yield-effects.md: -------------------------------------------------------------------------------- 1 | # Ensures effects are yielded (yield-effects) 2 | 3 | ![](https://img.shields.io/badge/-recommended-lightgrey.svg "recommended") ![suggestion fixable](https://img.shields.io/badge/-suggestion%20fixable-green.svg "Editors that supports eslint suggestions will provide a fix option") 4 | 5 | This rule ensures that all `redux-saga` effects are properly `yield`'ed. 6 | 7 | Not `yield`'ing an effect might result in strange control flow behaviour. 8 | 9 | ```es6 10 | import { take } from "redux-saga" 11 | 12 | // good 13 | function* good() { 14 | yield take("action") 15 | } 16 | 17 | // bad 18 | function* bad() { 19 | take("action") 20 | } 21 | ``` 22 | 23 | Note: There is no autofix for this rule since it would change the runtime behavior of the code and potentially break things. This would goes against the eslint [best practices for fixes](https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes-1). If you use an editor that supports the eslint [suggestions API](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions), however, (for example the [VSCode ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)), the editor will provide a suggestion for how to fix it. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-redux-saga", 3 | "version": "1.3.2", 4 | "description": "redux-saga eslint rules", 5 | "author": "Philipp Kursawe = 6.7.0", 36 | "redux-saga": ">= 0.11.1 < 1 || >= 1.0.0" 37 | }, 38 | "devDependencies": { 39 | "eslint": "8.4.1", 40 | "mocha": "9.1.3", 41 | "redux-saga": ">= 0.11.1 < 1 || >= 1.0.0" 42 | }, 43 | "keywords": [ 44 | "eslint", 45 | "eslint-plugin", 46 | "eslintplugin", 47 | "redux-saga" 48 | ], 49 | "license": "MIT", 50 | "dependencies": {} 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-redux-saga 2 | 3 | 4 | 5 | [![Build Status](https://img.shields.io/travis/pke/eslint-plugin-redux-saga/master.svg?style=flat-square)](https://travis-ci.org/pke/eslint-plugin-redux-saga) 6 | [![npm version](https://img.shields.io/npm/v/eslint-plugin-redux-saga.svg?style=flat-square)](https://badge.fury.io/js/eslint-plugin-redux-saga) 7 | [![License](https://img.shields.io/npm/l/eslint-plugin-redux-saga.svg?style=flat-square)](LICENSE) 8 | 9 | [ESLint](https://github.com/eslint/eslint) rules for [redux-saga](https://github.com/yelouafi/redux-saga). 10 | 11 | ## Usage 12 | 13 | Install the plugin: 14 | 15 | ### npm 16 | 17 | `npm i -D eslint-plugin-redux-saga` 18 | 19 | ### yarn 20 | 21 | `yarn add -D eslint-plugin-redux-saga` 22 | 23 | And add it to your `.eslintrc` file: 24 | 25 | ```json 26 | { 27 | "plugins": [ 28 | "redux-saga" 29 | ] 30 | } 31 | ``` 32 | 33 | ## Rules 34 | 35 | | Rule | Description | Recommended | Fixable | 36 | |-------------|------|-------------|---------| 37 | | [yield-effects](docs/rules/yield-effects.md) | Ensure effects are yielded | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | ![suggestion fixable](https://img.shields.io/badge/-suggestion%20fixable-green.svg) | 38 | | [no-yield-in-race](docs/rules/no-yield-in-race.md) | Prevent usage of yield in race entries | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | ![fixable](https://img.shields.io/badge/-fixable-green.svg) 39 | | [no-unhandled-errors](docs/rules/no-unhandled-errors.md) | Ensures error handling on sagas | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | 40 | 41 | ## Recommended configuration 42 | 43 | This plugin exports the `recommended` configuration that enforces all the rules. To use it, add following property to `.eslintrc` file: 44 | 45 | ```json 46 | { 47 | "extends": [ 48 | "plugin:redux-saga/recommended" 49 | ] 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /lib/rules/no-unhandled-errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rule to enforce error handling on sagas 3 | * @author Nicolas @BuraBure Fernandez 4 | */ 5 | /** eslint-disable semi */ 6 | "use strict" 7 | 8 | var Effects = require("../utils/effects") 9 | 10 | var nonCatchableEffects = ["fork", "takeEvery", "takeLatest", "takeLeading", "throttle"] 11 | var knowEffects = Effects.knowEffects().filter(function(item) { 12 | return nonCatchableEffects.indexOf(item) === -1 13 | }) 14 | 15 | module.exports = { 16 | meta: { 17 | docs: { 18 | description: "enforce error handling on sagas", 19 | category: "Possible Errors", 20 | recommended: true 21 | }, 22 | schema: [] 23 | }, 24 | 25 | create: function(context) { 26 | var effectLocalNames = [] 27 | var effectImportedNames = [] 28 | var inTryStatementDepth = 0 29 | return { 30 | "ImportDeclaration": function(node) { 31 | if (Effects.isEffectImport(node.source.value)) { 32 | node.specifiers.forEach(function(specifier) { 33 | if (specifier.type === "ImportSpecifier" && knowEffects.indexOf(specifier.imported.name) !== -1) { 34 | effectLocalNames.push(specifier.local.name) 35 | effectImportedNames.push(specifier.imported.name) 36 | } 37 | }) 38 | } 39 | }, 40 | "TryStatement": function() { 41 | inTryStatementDepth += 1 42 | }, 43 | "TryStatement:exit": function() { 44 | inTryStatementDepth -= 1 45 | }, 46 | "CallExpression": function(node) { 47 | var callee = node.callee 48 | var localNameIndex = effectLocalNames.indexOf(callee.name) 49 | if (localNameIndex !== -1) { 50 | var importedName = effectImportedNames[localNameIndex] 51 | var effectName = callee.name 52 | if (importedName !== effectName) { 53 | effectName += " (" + importedName + ")" 54 | } 55 | if (inTryStatementDepth === 0) { 56 | context.report({ 57 | node: node, 58 | message: "A Saga must handle its effects' errors (use try/catch)" 59 | }) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/lib/rules/no-yield-in-race-spec.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var rule = require("../../../lib/rules/no-yield-in-race") 3 | var RuleTester = require("eslint").RuleTester 4 | 5 | var ruleTester = new RuleTester() 6 | 7 | ruleTester.run("no-yield-in-race", rule, { 8 | valid: [ 9 | { 10 | code: "function* test() { yield race({ posts: call(fetchApis) }) }" 11 | }, 12 | { 13 | code: "function* test() { yield race({ watchers: [call(watcher1), call(watcher2)] }) }" 14 | }, 15 | { 16 | code: "function* test() { yield race([ call(watcher1), call(watcher2) ]) }" 17 | }, 18 | { 19 | code: "function* foo() { const actions = ['LOGIN', 'LOGOUT']; yield race(actions.map(take)) }" 20 | } 21 | ], 22 | invalid: [ 23 | { 24 | code: "function* test() { yield race({}, {}) }", 25 | errors: [{message: "race must have on object hash or array as the only argument"}] 26 | }, 27 | { 28 | code: "function* test() { yield race(call(fetch1), call(fetch2)) }", 29 | errors: [{message: "race must have on object hash or array as the only argument"}] 30 | }, 31 | { 32 | code: "function* test() { yield race({ posts: yield call(fetchApis) }) }", 33 | output: "function* test() { yield race({ posts: call(fetchApis) }) }", 34 | errors: [{message: "yield not allowed inside race: posts"}] 35 | }, 36 | { 37 | code: "function* test() { yield race({ watchers: yield [call(watcher1), call(watcher2)] }) }", 38 | output: "function* test() { yield race({ watchers: [call(watcher1), call(watcher2)] }) }", 39 | errors: [{message: "yield not allowed inside race: watchers"}] 40 | }, 41 | { 42 | code: "function* test() { yield race([yield call(watcher1), call(watcher2)]) }", 43 | output: "function* test() { yield race([call(watcher1), call(watcher2)]) }", 44 | errors: [{message: "yield not allowed inside race array at index: 0"}] 45 | }, 46 | { 47 | code: "function* foo() { const actions = ['LOGIN', 'LOGOUT']; yield race(yield actions.map(take)) }", 48 | output: "function* foo() { const actions = ['LOGIN', 'LOGOUT']; yield race(actions.map(take)) }", 49 | errors: [{message: "yield not allowed inside race"}] 50 | }, 51 | { 52 | code: "function* foo() { const actions = ['LOGIN', 'LOGOUT']; yield race(actions.every(take)) }", 53 | errors: [{message: "race must have Array.map as the only argument"}] 54 | } 55 | ] 56 | }) 57 | 58 | -------------------------------------------------------------------------------- /lib/rules/yield-effects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rule to enforce yield in front of effects 3 | * @author Philipp Kursawe 4 | */ 5 | /** eslint-disable semi */ 6 | "use strict" 7 | 8 | var Effects = require("../utils/effects") 9 | 10 | var knowEffects = Effects.knowEffects() 11 | 12 | module.exports = { 13 | meta: { 14 | docs: { 15 | description: "enforce yield in front of effects", 16 | category: "Possible Errors", 17 | recommended: true 18 | }, 19 | hasSuggestions: true, 20 | schema: [] 21 | }, 22 | 23 | create: function(context) { 24 | var inYieldDepth = 0 25 | var inGeneratorDepth = 0 26 | var effectLocalNames = [] 27 | var effectImportedNames = [] 28 | 29 | function enterFunction(node) { 30 | if (node.generator) { 31 | ++inGeneratorDepth 32 | } 33 | } 34 | function exitFunction(node) { 35 | if (node.generator) { 36 | --inGeneratorDepth 37 | } 38 | } 39 | return { 40 | "ImportDeclaration": function(node) { 41 | if (Effects.isEffectImport(node.source.value)) { 42 | node.specifiers.forEach(function(specifier) { 43 | if (specifier.type === "ImportSpecifier" && knowEffects.indexOf(specifier.imported.name) !== -1) { 44 | effectLocalNames.push(specifier.local.name) 45 | effectImportedNames.push(specifier.imported.name) 46 | } 47 | }) 48 | } 49 | }, 50 | "FunctionDeclaration": enterFunction, 51 | "FunctionDeclaration:exit": exitFunction, 52 | "FunctionExpression": enterFunction, 53 | "FunctionExpression:exit": exitFunction, 54 | "YieldExpression": function() { 55 | inYieldDepth += 1 56 | }, 57 | "YieldExpression:exit": function() { 58 | inYieldDepth -= 1 59 | }, 60 | "CallExpression": function(node) { 61 | var callee = node.callee 62 | var localNameIndex = effectLocalNames.indexOf(callee.name) 63 | if (localNameIndex !== -1) { 64 | var importedName = effectImportedNames[localNameIndex] 65 | var effectName = callee.name 66 | if (importedName !== effectName) { 67 | effectName += " (" + importedName + ")" 68 | } 69 | if (inYieldDepth === 0 && inGeneratorDepth) { 70 | context.report({ 71 | node: node, 72 | message: effectName + " effect must be yielded", 73 | suggest: [ 74 | { 75 | desc: "Add yield", 76 | fix: function(fixer) { 77 | return fixer.insertTextBefore(node, "yield ") 78 | } 79 | } 80 | ] 81 | }) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/lib/rules/no-unhandled-errors-spec.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var rule = require("../../../lib/rules/no-unhandled-errors") 3 | var RuleTester = require("eslint").RuleTester 4 | 5 | var ruleTester = new RuleTester() 6 | 7 | function buildTest(imports, code) { 8 | var result = (imports && (imports + ";")) || "" 9 | result += "function* test() { " + code + " }" 10 | return result 11 | } 12 | 13 | ruleTester.run("no-unhandled-errors", rule, { 14 | valid: [ 15 | { 16 | code: buildTest( 17 | "import { take } from 'redux-saga'", 18 | "try { yield take('ACTION') } catch (e) { errorHandler(e) }" 19 | ) 20 | }, 21 | { 22 | code: buildTest( 23 | "import { take as t } from 'redux-saga'", 24 | "try { yield t('ACTION') } catch (e) { errorHandler(e) }" 25 | ) 26 | }, 27 | { 28 | code: buildTest( 29 | "import { take } from 'redux-saga/effects'", 30 | "try { yield take('ACTION') } catch (e) { errorHandler(e) }" 31 | ) 32 | }, 33 | { 34 | code: buildTest(null, "try { yield take('ACTION') } catch (e) { errorHandler(e) }") 35 | }, 36 | { 37 | code: buildTest("import { take } from 'redux-saga'", "notAnEffectDoesNotNeedYield()") 38 | }, 39 | { 40 | code: "import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware();" 41 | }, 42 | { 43 | code: buildTest("import { noop } from 'redux-saga'", "noop()") 44 | }, 45 | { 46 | code: buildTest( 47 | "import { call } from 'redux-saga'", 48 | "try { yield call('ACTION') } catch (e) { errorHandler(e) }" 49 | ) 50 | }, 51 | { 52 | code: buildTest( 53 | "import { call } from 'redux-saga'", 54 | "try { someStuff() } catch (e) { yield call('ACTION') }" 55 | ) 56 | }, 57 | { 58 | code: buildTest( 59 | "import { call, fork } from 'redux-saga/effects'", 60 | "yield fork(function*(){})" 61 | ) 62 | }, 63 | { 64 | code: buildTest( 65 | "import { takeEvery, takeLatest, takeLeading, throttle } from 'redux-saga'", 66 | "yield takeEvery('ACTION', function*(){});" + 67 | "yield takeLatest('ACTION', function*(){});" + 68 | "yield takeLeading('ACTION', function*(){});" + 69 | "yield throttle('ACTION', function*(){})" 70 | ) 71 | } 72 | ], 73 | invalid: [ 74 | { 75 | code: buildTest("import { call } from 'redux-saga'", "yield call('ACTION')"), 76 | errors: [{message: "A Saga must handle its effects' errors (use try/catch)"}] 77 | }, 78 | { 79 | code: buildTest("import { put as p } from 'redux-saga'", "yield p('ACTION')"), 80 | errors: [{message: "A Saga must handle its effects' errors (use try/catch)"}] 81 | }, 82 | { 83 | code: buildTest( 84 | "import { call } from 'redux-saga'", 85 | "try { yield call('ACTION') } catch (e) { errorHandler(e) } yield call('ACTION')" 86 | ), 87 | errors: [{message: "A Saga must handle its effects' errors (use try/catch)"}] 88 | } 89 | ] 90 | }) 91 | -------------------------------------------------------------------------------- /lib/rules/no-yield-in-race.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rule to flag use of yield inside race effects 3 | * @author Philipp Kursawe 4 | */ 5 | "use strict" 6 | 7 | function checkYieldInObject(context, node) { 8 | var properties = node.arguments[0].properties 9 | for (var i = 0, len = properties.length; i < len; ++i) { 10 | var property = properties[i] 11 | var name = property.key.name 12 | if (property.value.type === "YieldExpression") { 13 | var yieldExpression = property.value 14 | context.report({ 15 | node: property, 16 | message: "yield not allowed inside race: " + name, 17 | fix: function(fixer) { 18 | var nextToken = property.value.argument 19 | return fixer.removeRange([yieldExpression.range[0], nextToken.range[0]]) 20 | } 21 | }) 22 | } 23 | } 24 | } 25 | 26 | function checkYieldInArray(context, node) { 27 | var elements = node.arguments[0].elements 28 | for (var i = 0, len = elements.length; i < len; ++i) { 29 | var element = elements[i] 30 | if (element.type === "YieldExpression") { 31 | var yieldExpression = element 32 | context.report({ 33 | node: element, 34 | message: "yield not allowed inside race array at index: " + i, 35 | fix: function(fixer) { 36 | var nextToken = element.argument 37 | return fixer.removeRange([yieldExpression.range[0], nextToken.range[0]]) 38 | } 39 | }) 40 | } 41 | } 42 | } 43 | 44 | function checkCallExpression(context, node) { 45 | var callee = node.arguments[0].callee 46 | var object = callee && callee.object 47 | if (object) { 48 | var name = object.name 49 | var foundVariable = context.getScope().variables.find(function(variable) { 50 | return variable.name === name 51 | }) 52 | if (foundVariable) { 53 | var def = foundVariable.defs[foundVariable.defs.length - 1] 54 | if (def.node.init.type === "ArrayExpression" && callee.property.name !== "map") { 55 | context.report(node, "race must have Array.map as the only argument") 56 | } 57 | } 58 | } 59 | } 60 | 61 | module.exports = { 62 | meta: { 63 | docs: { 64 | description: "flag use of yield inside race effects", 65 | category: "Possible Errors", 66 | recommended: true 67 | }, 68 | fixable: "code", 69 | schema: [] 70 | }, 71 | 72 | create: function(context) { 73 | return { 74 | "CallExpression": function(node) { 75 | var callee = node.callee 76 | if (callee.name === "race") { 77 | var type = node.arguments[0].type 78 | if (node.arguments.length !== 1) { 79 | context.report(node, "race must have on object hash or array as the only argument") 80 | } else if (type === "ObjectExpression") { 81 | checkYieldInObject(context, node) 82 | } else if (type === "ArrayExpression") { 83 | checkYieldInArray(context, node) 84 | } else if (type === "CallExpression") { 85 | checkCallExpression(context, node) 86 | } else if (type === "YieldExpression") { 87 | var yieldExpression = node.arguments[0] 88 | context.report({ 89 | node: yieldExpression, 90 | message: "yield not allowed inside race", 91 | fix: function(fixer) { 92 | var nextToken = yieldExpression.argument 93 | return fixer.removeRange([yieldExpression.range[0], nextToken.range[0]]) 94 | } 95 | }) 96 | return 97 | } else { 98 | context.report(node, "race must have on object hash or array as the only argument") 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parserOptions": { 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "globals": { 11 | }, 12 | "plugins": [ 13 | ], 14 | "rules": { 15 | // Possible Errors 16 | "comma-dangle": [2, "never"], 17 | "no-cond-assign": 2, 18 | "no-console": 2, 19 | "no-constant-condition": 2, 20 | "no-control-regex": 2, 21 | "no-debugger": 2, 22 | "no-dupe-keys": 2, 23 | "no-empty": 2, 24 | "no-empty-character-class": 2, 25 | "no-ex-assign": 2, 26 | "no-extra-boolean-cast": 2, 27 | "no-extra-parens": 0, 28 | "no-extra-semi": 2, 29 | "no-func-assign": 2, 30 | "no-inner-declarations": 2, 31 | "no-invalid-regexp": 2, 32 | "no-irregular-whitespace": 2, 33 | "no-negated-in-lhs": 2, 34 | "no-obj-calls": 2, 35 | "no-regex-spaces": 2, 36 | "no-reserved-keys": 0, 37 | "no-sparse-arrays": 2, 38 | "no-unreachable": 2, 39 | "use-isnan": 2, 40 | "valid-jsdoc": 0, 41 | "valid-typeof": 2, 42 | // Best Practices 43 | "block-scoped-var": 2, 44 | "complexity": 0, 45 | "consistent-return": 2, 46 | "curly": 2, 47 | "default-case": 2, 48 | "dot-notation": 2, 49 | "eqeqeq": 2, 50 | "guard-for-in": 2, 51 | "no-alert": 2, 52 | "no-caller": 2, 53 | "no-div-regex": 2, 54 | "no-else-return": 2, 55 | "no-eq-null": 2, 56 | "no-eval": 2, 57 | "no-extend-native": 2, 58 | "no-extra-bind": 2, 59 | "no-fallthrough": 2, 60 | "no-floating-decimal": 2, 61 | "no-implied-eval": 2, 62 | "no-iterator": 2, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-loop-func": 0, 66 | "no-multi-spaces": 2, 67 | "no-multi-str": 0, 68 | "no-native-reassign": 2, 69 | "no-new": 2, 70 | "no-new-func": 2, 71 | "no-new-wrappers": 2, 72 | "no-octal": 2, 73 | "no-octal-escape": 2, 74 | "no-process-env": 2, 75 | "no-proto": 2, 76 | "no-redeclare": 2, 77 | "no-return-assign": 2, 78 | "no-script-url": 2, 79 | "no-self-compare": 2, 80 | "no-sequences": 2, 81 | "no-unused-expressions": 2, 82 | "no-void": 0, 83 | "no-warning-comments": 2, 84 | "no-with": 2, 85 | "radix": 2, 86 | "vars-on-top": 0, 87 | "wrap-iife": 2, 88 | "yoda": 2, 89 | // Strict Mode 90 | "strict": [2, "global"], 91 | // Variables 92 | "no-catch-shadow": 2, 93 | "no-delete-var": 2, 94 | "no-label-var": 2, 95 | "no-shadow": 2, 96 | "no-shadow-restricted-names": 2, 97 | "no-undef": 2, 98 | "no-undef-init": 2, 99 | "no-undefined": 2, 100 | "no-unused-vars": 2, 101 | "no-use-before-define": 2, 102 | // Stylistic Issues 103 | "indent": [2, 2, { 104 | "SwitchCase": 1 105 | }], 106 | "brace-style": 2, 107 | "camelcase": 0, 108 | "comma-spacing": 2, 109 | "comma-style": 2, 110 | "consistent-this": 0, 111 | "eol-last": 2, 112 | "func-names": 0, 113 | "func-style": 0, 114 | "key-spacing": [2, { 115 | "beforeColon": false, 116 | "afterColon": true 117 | }], 118 | "max-nested-callbacks": 0, 119 | "new-cap": 2, 120 | "new-parens": 2, 121 | "no-array-constructor": 2, 122 | "no-inline-comments": 0, 123 | "no-lonely-if": 2, 124 | "no-mixed-spaces-and-tabs": 2, 125 | "no-nested-ternary": 2, 126 | "no-new-object": 2, 127 | "semi-spacing": [2, { 128 | "before": false, 129 | "after": true 130 | }], 131 | "no-spaced-func": 2, 132 | "no-ternary": 0, 133 | "no-trailing-spaces": 2, 134 | "no-multiple-empty-lines": 2, 135 | "no-underscore-dangle": 0, 136 | "one-var": 0, 137 | "operator-assignment": [2, "always"], 138 | "padded-blocks": 0, 139 | "quotes": [2, "double"], 140 | "semi": [2, "never"], 141 | "sort-vars": [2, {"ignoreCase": true}], 142 | "keyword-spacing": 2, 143 | "space-before-blocks": 2, 144 | "object-curly-spacing": [2, "never"], 145 | "array-bracket-spacing": [2, "never"], 146 | "space-in-parens": 2, 147 | "space-infix-ops": 2, 148 | "space-unary-ops": 2, 149 | "spaced-comment": 2, 150 | "wrap-regex": 0, 151 | // Legacy 152 | "max-depth": 0, 153 | "max-len": [2, 120], 154 | "max-params": 0, 155 | "max-statements": 0, 156 | "no-plusplus": 0 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/lib/rules/yield-effects-spec.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var rule = require("../../../lib/rules/yield-effects") 3 | var RuleTester = require("eslint").RuleTester 4 | 5 | var ruleTester = new RuleTester() 6 | 7 | function buildTest(imports, code) { 8 | var result = (imports && (imports + ";")) || "" 9 | result += "function* test() { " + code + " }" 10 | return result 11 | } 12 | 13 | ruleTester.run("yield-effects", rule, { 14 | valid: [ 15 | { 16 | code: buildTest("import { take } from 'redux-saga'", "yield take('ACTION')") 17 | }, 18 | { 19 | code: buildTest("import { delay } from 'redux-saga'", "yield delay(1000)") 20 | }, 21 | { 22 | code: buildTest("import { take } from 'redux-saga/effects'", "yield take('ACTION')") 23 | }, 24 | { 25 | code: buildTest("import { take as t } from 'redux-saga'", "yield t('ACTION')") 26 | }, 27 | { 28 | code: buildTest("import { delay as d } from 'redux-saga'", "yield d(1000)") 29 | }, 30 | { 31 | // If it is an effect name but not imported from `redux-saga` then its valid 32 | code: buildTest(null, "take('ACTION')") 33 | }, 34 | { 35 | // If it is an effect name but not imported from `redux-saga` then its valid 36 | code: buildTest(null, "delay(1000)") 37 | }, 38 | { 39 | code: buildTest("import { take } from 'redux-saga'", "notAnEffectDoesNotNeedYield()") 40 | }, 41 | { 42 | code: "import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware();" 43 | }, 44 | { 45 | code: buildTest("import { noop } from 'redux-saga'", "noop()") 46 | }, 47 | { 48 | code: buildTest( 49 | "import { call } from 'redux-saga'", 50 | "const [foo, bar] = yield [call(something), call(somethingElse)]" 51 | ) 52 | }, 53 | { 54 | code: 55 | "import { put } from 'redux-saga';\n" + 56 | "expect(generator.next().value).toEqual(put({}));" 57 | }, 58 | { 59 | code: buildTest( 60 | "import { call, all, delay, fetchResources } from 'redux-saga'", 61 | "yield all([" + 62 | "call(fetchResource, 'users')," + 63 | "call(fetchResource, 'comments')," + 64 | "call(delay, 1000)" + 65 | "])" 66 | ) 67 | }, 68 | { 69 | code: 70 | "import { takeEvery } from 'redux-saga';\n" + 71 | "export const fooSagas = [" + 72 | "takeEvery('FOO_A', fooASaga)," + 73 | "takeEvery('FOO_B', fooBSaga)];" 74 | }, 75 | { 76 | code: 77 | "import { call } from 'redux-saga';\n" + 78 | "export class FooSaga {" + 79 | "static* someSaga() {" + 80 | " yield call(() => {})" + 81 | "}" + 82 | "}" 83 | } 84 | ], 85 | invalid: [ 86 | { 87 | code: buildTest("import { take } from 'redux-saga'", "take('ACTION')"), 88 | errors: [{ 89 | message: "take effect must be yielded", 90 | suggestions: [ 91 | { 92 | desc: "Add yield", 93 | output: buildTest("import { take } from 'redux-saga'", "yield take('ACTION')") 94 | } 95 | ] 96 | }] 97 | }, 98 | { 99 | code: buildTest("import { delay } from 'redux-saga'", "delay('ACTION')"), 100 | errors: [{ 101 | message: "delay effect must be yielded", 102 | suggestions: [ 103 | { 104 | desc: "Add yield", 105 | output: buildTest("import { delay } from 'redux-saga'", "yield delay('ACTION')") 106 | } 107 | ] 108 | }] 109 | }, 110 | { 111 | code: buildTest("import { take as t } from 'redux-saga'", "t('ACTION')"), 112 | errors: [{ 113 | message: "t (take) effect must be yielded", 114 | suggestions: [ 115 | { 116 | desc: "Add yield", 117 | output: buildTest("import { take as t } from 'redux-saga'", "yield t('ACTION')") 118 | } 119 | ] 120 | }] 121 | }, 122 | { 123 | code: buildTest("import { delay as d } from 'redux-saga'", "d('ACTION')"), 124 | errors: [{ 125 | message: "d (delay) effect must be yielded", 126 | suggestions: [ 127 | { 128 | desc: "Add yield", 129 | output: buildTest("import { delay as d } from 'redux-saga'", "yield d('ACTION')") 130 | } 131 | ] 132 | }] 133 | }, 134 | { 135 | code: 136 | "import { call } from 'redux-saga';\n" + 137 | "export class FooSaga {" + 138 | "static* someSaga() {" + 139 | " call(() => {})" + 140 | "}" + 141 | "}", 142 | errors: [{ 143 | message: "call effect must be yielded", 144 | suggestions: [ 145 | { 146 | desc: "Add yield", 147 | output: 148 | "import { call } from 'redux-saga';\n" + 149 | "export class FooSaga {" + 150 | "static* someSaga() {" + 151 | " yield call(() => {})" + 152 | "}" + 153 | "}" 154 | } 155 | ] 156 | }] 157 | } 158 | ] 159 | }) 160 | --------------------------------------------------------------------------------