├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .release-it.js ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── index.js ├── package.json ├── renovate.json ├── rules ├── no-debug.js ├── no-debug.md ├── no-debug.test.js ├── no-native-promise-helpers.js ├── no-native-promise-helpers.md ├── no-native-promise-helpers.test.js ├── no-perform-without-catch.js ├── no-perform-without-catch.md ├── no-perform-without-catch.test.js ├── require-task-name-suffix.js ├── require-task-name-suffix.md └── require-task-name-suffix.test.js ├── utils └── utils.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | ecmaVersion: '2019', 7 | }, 8 | plugins: ['prettier'], 9 | extends: ['eslint:recommended', 'prettier'], 10 | env: { 11 | node: true, 12 | }, 13 | rules: { 14 | 'prettier/prettier': 'error', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'v*' 8 | pull_request: {} 9 | schedule: 10 | - cron: '0 3 * * *' # daily, at 3am 11 | 12 | env: 13 | FORCE_COLOR: 1 14 | 15 | jobs: 16 | lint: 17 | name: Linting 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 14.x 25 | 26 | - run: yarn install 27 | - run: yarn lint 28 | 29 | test-node: 30 | strategy: 31 | matrix: 32 | node-version: [10.x, 12.x, 14.x] 33 | 34 | name: Tests (Node.js ${{ matrix.node-version }}) 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-node@v3 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | 43 | - run: yarn install 44 | - run: yarn test --coverage 45 | 46 | test-eslint: 47 | strategy: 48 | matrix: 49 | eslint-version: [7.0.0, 6.0.0] 50 | 51 | name: Tests (ESLint ${{ matrix.eslint-version }}) 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v3 56 | - uses: actions/setup-node@v3 57 | with: 58 | node-version: 14.x 59 | 60 | - run: yarn install 61 | - run: yarn add --dev eslint@${{ matrix.eslint-version }} 62 | - run: yarn test 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 12.x 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | 117 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.test.js 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | printWidth: 100, 5 | semi: true, 6 | arrowParens: 'avoid', 7 | singleQuote: true, 8 | trailingComma: 'es5', 9 | }; 10 | -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'release-it-lerna-changelog': { 4 | infile: 'CHANGELOG.md', 5 | }, 6 | }, 7 | git: { 8 | commitMessage: 'v${version}', 9 | tagName: 'v${version}', 10 | }, 11 | github: { 12 | release: true, 13 | releaseName: 'v${version}', 14 | tokenRef: 'GITHUB_AUTH', 15 | }, 16 | npm: { 17 | publish: false, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## v0.5.1 (2021-11-05) 6 | 7 | ## v0.5.0 (2021-10-14) 8 | 9 | #### :rocket: Enhancement 10 | * [#151](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/151) Adding the `no-debug` rule ([@Mikek2252](https://github.com/Mikek2252)) 11 | 12 | #### :bug: Bug Fix 13 | * [#150](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/150) Update `hasTaskDecorator` helper to include CallExpression as a valid expression Type ([@Mikek2252](https://github.com/Mikek2252)) 14 | 15 | #### :memo: Documentation 16 | * [#153](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/153) README: Include `no-debug` rule ([@Mikek2252](https://github.com/Mikek2252)) 17 | 18 | #### :house: Internal 19 | * [#152](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/152) Move `hasTaskCallExpression` to utils ([@Mikek2252](https://github.com/Mikek2252)) 20 | 21 | #### Committers: 1 22 | - Michael Kerr ([@Mikek2252](https://github.com/Mikek2252)) 23 | 24 | ## v0.4.0 (2021-08-31) 25 | 26 | #### :memo: Documentation 27 | * [#57](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/57) Fix "no-native-promise-helpers" link ([@bertdeblock](https://github.com/bertdeblock)) 28 | 29 | #### Committers: 2 30 | - Bert De Block ([@bertdeblock](https://github.com/bertdeblock)) 31 | - Michael Kerr ([@Mikek2252](https://github.com/Mikek2252)) 32 | 33 | ## v0.3.1 (2020-12-30) 34 | 35 | #### :bug: Bug Fix 36 | * [#54](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/54) Correctly export `no-native-promise-helpers` rule ([@Turbo87](https://github.com/Turbo87)) 37 | 38 | #### Committers: 1 39 | - Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) 40 | 41 | ## v0.3.0 (2020-12-30) 42 | 43 | #### :rocket: Enhancement 44 | * [#53](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/53) Implement `no-native-promise-helpers` rule ([@Turbo87](https://github.com/Turbo87)) 45 | 46 | #### Committers: 1 47 | - Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) 48 | 49 | ## v0.2.1 (2020-10-29) 50 | 51 | #### :rocket: Enhancement 52 | * [#26](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/26) no-perform-without-catch: Adjust report node ([@Turbo87](https://github.com/Turbo87)) 53 | 54 | #### Committers: 1 55 | - Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) 56 | 57 | ## v0.2.0 (2020-10-29) 58 | 59 | #### :rocket: Enhancement 60 | * [#19](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/19) Add support for ESLint 7 ([@Turbo87](https://github.com/Turbo87)) 61 | * [#13](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/13) Implement `no-perform-without-catch` rule ([@Turbo87](https://github.com/Turbo87)) 62 | 63 | #### :house: Internal 64 | * [#14](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/14) Configure Renovate ([@renovate[bot]](https://github.com/apps/renovate)) 65 | * [#15](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/15) Various CI improvements ([@Turbo87](https://github.com/Turbo87)) 66 | 67 | #### Committers: 1 68 | - Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) 69 | 70 | ## v0.1.1 (2020-04-17) 71 | 72 | #### :rocket: Enhancement 73 | * [#9](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/9) require-task-name-suffix: Add support for `@task` decorators ([@Turbo87](https://github.com/Turbo87)) 74 | 75 | #### :bug: Bug Fix 76 | * [#12](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/12) package.json: Fix `repository` field ([@Turbo87](https://github.com/Turbo87)) 77 | 78 | #### :house: Internal 79 | * [#11](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/11) CI: Force colored output ([@Turbo87](https://github.com/Turbo87)) 80 | * [#10](https://github.com/Mainmatter/eslint-plugin-ember-concurrency/pull/10) CI: Run tests with `--coverage` ([@Turbo87](https://github.com/Turbo87)) 81 | 82 | #### Committers: 1 83 | - Tobias Bieniek ([@Turbo87](https://github.com/Turbo87)) 84 | 85 | 86 | ## v0.1.0 (2020-04-17) 87 | 88 | Initial release! 🎉 89 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 - 2024 Mainmatter GmbH and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | eslint-plugin-ember-concurrency 2 | ============================================================================== 3 | 4 | [ESLint] plugin for [ember-concurrency] 5 | 6 | [ESLint]: https://eslint.org/ 7 | [ember-concurrency]: http://ember-concurrency.com 8 | 9 | 10 | Installation 11 | ------------------------------------------------------------------------------ 12 | 13 | ```bash 14 | yarn add --dev eslint-plugin-ember-concurrency 15 | ``` 16 | 17 | 18 | Usage 19 | ------------------------------------------------------------------------------ 20 | 21 | ```js 22 | // .eslintrc.js 23 | 24 | module.exports = { 25 | plugins: ['ember-concurrency'], 26 | 27 | rules: { 28 | 'ember-concurrency/no-perform-without-catch': 'error', 29 | 'ember-concurrency/require-task-name-suffix': 'error', 30 | }, 31 | }; 32 | ``` 33 | 34 | 35 | Rules 36 | ------------------------------------------------------------------------------ 37 | 38 | - [no-native-promise-helpers](./rules/no-native-promise-helpers.md) – Prevents 39 | usage of `Promise.all/race()` in tasks 40 | 41 | - [no-perform-without-catch](./rules/no-perform-without-catch.md) – Ensures 42 | all `.perform()` calls have some kind of error handling 43 | 44 | - [require-task-name-suffix](./rules/require-task-name-suffix.md) – Ensures 45 | consistent task property names 46 | 47 | - [no-debug](./rules/no-debug.md) - Ensures task debuggers are not shipped to production 48 | 49 | 50 | License 51 | ------------------------------------------------------------------------------ 52 | 53 | This projects is developed by and © [Mainmatter GmbH](http://mainmatter.com) 54 | and contributors. It is released under the [MIT License](LICENSE.md). 55 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | 8 | ## Preparation 9 | 10 | Since the majority of the actual release process is automated, the primary 11 | remaining task prior to releasing is confirming that all pull requests that 12 | have been merged since the last release have been labeled with the appropriate 13 | `lerna-changelog` labels and the titles have been updated to ensure they 14 | represent something that would make sense to our users. Some great information 15 | on why this is important can be found at 16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 17 | guiding principle here is that changelogs are for humans, not machines. 18 | 19 | When reviewing merged PR's the labels to be used are: 20 | 21 | * breaking - Used when the PR is considered a breaking change. 22 | * enhancement - Used when the PR adds a new feature or enhancement. 23 | * bug - Used when the PR fixes a bug included in a previous release. 24 | * documentation - Used when the PR adds or updates documentation. 25 | * internal - Used for internal changes that still require a mention in the 26 | changelog/release notes. 27 | 28 | 29 | ## Release 30 | 31 | Once the prep work is completed, the actual release is straight forward: 32 | 33 | * First, ensure that you have an environment variable with your GitHub token 34 | setup as `GITHUB_AUTH`. This token will be used for generating your changelog 35 | (unauthenticated requests to the GitHub API are heavily throttled) and for 36 | creating the GitHub release. Only "repo" access is needed; no "admin" 37 | or other scopes are required. 38 | 39 | * Next, ensure that you have installed your projects dependencies: 40 | 41 | ``` 42 | yarn install 43 | ``` 44 | 45 | * And last (but not least 😁) do your release: 46 | 47 | ``` 48 | yarn release 49 | ``` 50 | 51 | [release-it](https://github.com/release-it/release-it/) manages the actual 52 | release process. It will prompt you to to choose the version number after which 53 | you will have the chance to hand tweak the changelog to be used (for the 54 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 55 | pushing the tag and commits, etc. Finally GitHub Actions will build the commit 56 | and push the release to npm. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | rules: { 5 | 'no-native-promise-helpers': require('./rules/no-native-promise-helpers'), 6 | 'no-perform-without-catch': require('./rules/no-perform-without-catch'), 7 | 'require-task-name-suffix': require('./rules/require-task-name-suffix'), 8 | 'no-debug': require('./rules/no-debug'), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-ember-concurrency", 3 | "version": "0.5.1", 4 | "description": "ESLint plugin for ember-concurrency users", 5 | "repository": "https://github.com/Mainmatter/eslint-plugin-ember-concurrency", 6 | "license": "MIT", 7 | "author": "Tobias Bieniek ", 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint . --cache", 11 | "release": "release-it", 12 | "test": "jest" 13 | }, 14 | "devDependencies": { 15 | "babel-eslint": "10.1.0", 16 | "eslint": "7.32.0", 17 | "eslint-config-prettier": "10.1.2", 18 | "eslint-plugin-prettier": "4.0.0", 19 | "jest": "27.5.1", 20 | "prettier": "2.8.8", 21 | "release-it": "14.14.3", 22 | "release-it-lerna-changelog": "3.1.0" 23 | }, 24 | "peerDependencies": { 25 | "eslint": "6.* || 7.*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib", 4 | ":automergePatch", 5 | ":automergeLinters", 6 | ":automergeTesters", 7 | ":dependencyDashboard", 8 | ":maintainLockFilesWeekly", 9 | ":semanticCommitsDisabled" 10 | ], 11 | "packageRules": [ 12 | { 13 | "matchCurrentVersion": ">= 1.0.0", 14 | "updateTypes": [ 15 | "minor" 16 | ], 17 | "automerge": true 18 | }, 19 | { 20 | "packageNames": [ 21 | "ember-cli", 22 | "ember-data", 23 | "ember-source" 24 | ], 25 | "separateMinorPatch": true 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /rules/no-debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { hasTaskDecorator, hasTaskCallExpression } = require('../utils/utils'); 4 | 5 | module.exports = { 6 | create(context) { 7 | return { 8 | Property(node) { 9 | if (node.method || node.shorthand || node.computed) return; 10 | let { key, value } = node; 11 | if (hasTaskCallExpression(value) && hasDebugCallee(value)) { 12 | context.report({ 13 | node: key, 14 | message: 'Unexpected task debugger', 15 | }); 16 | } 17 | }, 18 | MethodDefinition(node) { 19 | let { key, value, decorators } = node; 20 | if (!decorators) return; 21 | if (!value) return; 22 | if (!value.generator) return; 23 | if (hasTaskDecorator(node) && hasDebugArgument(node)) { 24 | context.report({ 25 | node: key, 26 | message: 'Unexpected task debugger', 27 | }); 28 | } 29 | }, 30 | ClassProperty(node) { 31 | if (node.static || node.computed) return; 32 | 33 | let { key, value, decorators } = node; 34 | if (value !== null) return; 35 | if (!decorators) return; 36 | 37 | for (let decorator of node.decorators) { 38 | if (hasTaskCallExpression(decorator.expression) && hasDebugCallee(decorator.expression)) { 39 | context.report({ 40 | node: key, 41 | message: 'Unexpected task debugger', 42 | }); 43 | } 44 | } 45 | }, 46 | }; 47 | }, 48 | }; 49 | 50 | function hasDebugCallee(node) { 51 | let { callee } = node; 52 | return ( 53 | callee.type === 'MemberExpression' && 54 | callee.property && 55 | callee.property.type === 'Identifier' && 56 | callee.property.name === 'debug' 57 | ); 58 | } 59 | 60 | function hasDebugArgument(node) { 61 | if (!node.decorators) return false; 62 | 63 | return node.decorators.some(decorator => { 64 | let { expression } = decorator; 65 | 66 | if (!expression) return false; 67 | if (!expression.arguments) return false; 68 | 69 | return expression.arguments.some(argument => { 70 | if (argument.type !== 'ObjectExpression') return false; 71 | 72 | return argument.properties.some(property => { 73 | return property.key && property.key.name === 'debug'; 74 | }); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /rules/no-debug.md: -------------------------------------------------------------------------------- 1 | # no-debug 2 | 3 | This rule ensures that all `ember-concurrency` tasks in the app 4 | do not have debug shipped into production. 5 | 6 | 7 | ## Examples 8 | 9 | This rule **forbids** the following: 10 | 11 | ```js 12 | export default Component.extend({ 13 | submit: task(function*() { 14 | //... 15 | }).debug(), 16 | }) 17 | ``` 18 | 19 | ```js 20 | export default class extends Component { 21 | @(task(function*() { 22 | //... 23 | }).debug()) submitTask; 24 | } 25 | ``` 26 | 27 | ```js 28 | export default class extends Component { 29 | @task({ debug: true }) *submitTask() { 30 | //... 31 | } 32 | } 33 | 34 | ``` 35 | 36 | This rule **allows** the following: 37 | 38 | ```js 39 | export default class extends Component { 40 | @task *submitTask() { }; 41 | } 42 | ``` 43 | 44 | ```js 45 | export default Component.extend({ 46 | submit: task(function*() { 47 | //... 48 | }).debug(), 49 | }) 50 | ``` 51 | 52 | ```js 53 | export default class extends Component { 54 | @(task(function*() { 55 | //... 56 | }).debug()) submitTask; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /rules/no-debug.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require('eslint'); 2 | 3 | const rule = require('./no-debug'); 4 | 5 | let VALID = [`export default Component.extend({ submitTask: task(function*() {}) });`]; 6 | 7 | let INVALID = [ 8 | { 9 | code: `export default Component.extend({ submitTask: task(function*() {}).debug() });`, 10 | errors: [{ message: 'Unexpected task debugger', column: 35 }], 11 | }, 12 | ]; 13 | 14 | let VALID_BABEL = [ 15 | `export default class extends Component { @task *submitTask() { }; }`, 16 | `export default class extends Component { @restartableTask *submitTask() { }; }`, 17 | `export default class extends Component { @dropTask *submitTask() { }; }`, 18 | `export default class extends Component { @keepLatestTask *submitTask() { }; }`, 19 | `export default class extends Component { @enqueueTask *submitTask() { }; }`, 20 | `export default class extends Component { @task(function*() {}) submitTask; }`, 21 | `export default class extends Component { @(task(function*() {})) submitTask; }`, 22 | ]; 23 | 24 | let INVALID_BABEL = [ 25 | { 26 | code: `export default class extends Component { @(task(function*() {}).debug()) submitTask; }`, 27 | errors: [{ message: 'Unexpected task debugger', column: 74 }], 28 | }, 29 | { 30 | code: `export default class extends Component { @(task(function*() {}).debug()) submitTask; }`, 31 | errors: [{ message: 'Unexpected task debugger', column: 74 }], 32 | }, 33 | { 34 | code: `export default class extends Component { @task({ debug: true }) *submitTask() { } }`, 35 | errors: [{ message: 'Unexpected task debugger', column: 66 }], 36 | }, 37 | { 38 | code: `export default class extends Component { @restartableTask({ debug: true }) *submitTask() { } }`, 39 | errors: [{ message: 'Unexpected task debugger', column: 77 }], 40 | }, 41 | { 42 | code: `export default class extends Component { @dropTask({ debug: true }) *submitTask() { } }`, 43 | errors: [{ message: 'Unexpected task debugger', column: 70 }], 44 | }, 45 | { 46 | code: `export default class extends Component { @keepLatestTask({ debug: true }) *submitTask() { } }`, 47 | errors: [{ message: 'Unexpected task debugger', column: 76 }], 48 | }, 49 | { 50 | code: `export default class extends Component { @enqueueTask({ debug: true }) *submitTask() { } }`, 51 | errors: [{ message: 'Unexpected task debugger', column: 73 }], 52 | }, 53 | ]; 54 | 55 | let ruleTester = new RuleTester({ 56 | parserOptions: { 57 | ecmaVersion: 2018, 58 | sourceType: 'module', 59 | }, 60 | }); 61 | 62 | ruleTester.run('no-debug', rule, { 63 | valid: VALID, 64 | invalid: INVALID, 65 | }); 66 | 67 | let babelRuleTester = new RuleTester({ 68 | parser: require.resolve('babel-eslint'), 69 | parserOptions: { 70 | ecmaVersion: 2018, 71 | sourceType: 'module', 72 | }, 73 | }); 74 | 75 | babelRuleTester.run('no-debug', rule, { 76 | valid: [...VALID, ...VALID_BABEL], 77 | invalid: [...INVALID, ...INVALID_BABEL], 78 | }); 79 | -------------------------------------------------------------------------------- /rules/no-native-promise-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HELPERS = ['all', 'race', 'allSettled']; 4 | 5 | const { hasTaskDecorator } = require('../utils/utils'); 6 | 7 | module.exports = { 8 | create(context) { 9 | let parentTask = null; 10 | 11 | return { 12 | FunctionExpression(node) { 13 | if (parentTask) return; 14 | 15 | let { generator, parent } = node; 16 | if (!generator) return; 17 | if (parent.type !== 'CallExpression' || parent.arguments[0] !== node) return; 18 | 19 | let { callee } = parent; 20 | if (callee.type !== 'Identifier' || callee.name !== 'task') return; 21 | 22 | parentTask = node; 23 | }, 24 | 25 | 'FunctionExpression:exit'(node) { 26 | if (node === parentTask) { 27 | parentTask = null; 28 | } 29 | }, 30 | 31 | MethodDefinition(node) { 32 | if (parentTask) return; 33 | let { value, decorators } = node; 34 | if (!decorators) return; 35 | if (!value) return; 36 | if (!value.generator) return; 37 | 38 | if (hasTaskDecorator(node)) { 39 | parentTask = node; 40 | } 41 | }, 42 | 43 | 'MethodDefinition:exit'(node) { 44 | if (node === parentTask) { 45 | parentTask = null; 46 | } 47 | }, 48 | 49 | CallExpression(node) { 50 | if (!parentTask) return; 51 | 52 | let { callee } = node; 53 | if (callee.type !== 'MemberExpression') return; 54 | 55 | let { object, property } = callee; 56 | if (object.type !== 'Identifier' || object.name !== 'Promise') return; 57 | if (property.type !== 'Identifier' || !HELPERS.includes(property.name)) return; 58 | 59 | context.report({ 60 | node, 61 | message: `Use \`import { ${property.name} } from 'ember-concurrency';\` instead of \`Promise.${property.name}()\``, 62 | }); 63 | }, 64 | }; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /rules/no-native-promise-helpers.md: -------------------------------------------------------------------------------- 1 | # no-native-promise-helpers 2 | 3 | `Promise.all()` and `Promise.race()` do not cancel their child promises or 4 | tasks, because they are not aware of cancellation in the way that 5 | ember-concurrency has implemented it. ember-concurrency does provide 6 | alternative, cancellation-aware `all()` and `race()` implementations though, 7 | but they need to be explicitly imported. 8 | 9 | see 10 | 11 | This rule warns about usage of `Promise.all()` and `Promise.race()` inside of 12 | ember-concurrency tasks. 13 | 14 | ## Examples 15 | 16 | This rule **forbids** the following: 17 | 18 | ```js 19 | submitTask: task(function*() { 20 | yield Promise.all([ 21 | this.saveTask.perform(), 22 | this.loadingSpinnerTask.perform(), 23 | ]); 24 | }) 25 | ``` 26 | 27 | This rule **allows** the following: 28 | 29 | ```js 30 | import { all } from 'ember-concurrency'; 31 | 32 | submitTask: task(function*() { 33 | yield all([ 34 | this.saveTask.perform(), 35 | this.loadingSpinnerTask.perform(), 36 | ]); 37 | }) 38 | ``` 39 | -------------------------------------------------------------------------------- /rules/no-native-promise-helpers.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require('eslint'); 2 | 3 | const rule = require('./no-native-promise-helpers'); 4 | 5 | let ruleTester = new RuleTester({ 6 | parser: require.resolve('babel-eslint'), 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | }, 11 | }); 12 | 13 | ruleTester.run('no-native-promise-helpers', rule, { 14 | valid: [ 15 | ` 16 | import { all } from 'ember-concurrency'; 17 | 18 | export default Component.extend({ 19 | submitTask: task(function*() { 20 | yield all([ 21 | this.saveTask.perform(), 22 | this.loadingSpinnerTask.perform(), 23 | ]); 24 | }) 25 | }); 26 | `, 27 | ` 28 | import { all } from 'ember-concurrency'; 29 | 30 | export default class extends Component { 31 | @task(function*() { 32 | yield all([ 33 | this.saveTask.perform(), 34 | this.loadingSpinnerTask.perform(), 35 | ]); 36 | }) 37 | submitTask; 38 | } 39 | `, 40 | ` 41 | import { all } from 'ember-concurrency'; 42 | 43 | export default Component.extend({ 44 | async submit() { 45 | await Promise.all([foo(), bar()]); 46 | }, 47 | }); 48 | `, 49 | ` 50 | import { all } from 'ember-concurrency'; 51 | 52 | export default class extends Component { 53 | async submit() { 54 | await Promise.all([foo(), bar()]); 55 | } 56 | } 57 | `, 58 | ` 59 | import { all } from 'ember-concurrency'; 60 | 61 | export default class extends Component { 62 | *submit() { 63 | yield Promise.all([foo(), bar()]); 64 | } 65 | } 66 | `, 67 | ` 68 | export default Component.extend({ 69 | submitTask: foo(function*() { 70 | yield Promise.all([ 71 | this.saveTask.perform(), 72 | this.loadingSpinnerTask.perform(), 73 | ]); 74 | }) 75 | }); 76 | `, 77 | ` 78 | import { all } from 'ember-concurrency'; 79 | 80 | export default class extends Component { 81 | @task *submitTask() { 82 | yield all([ 83 | this.saveTask.perform(), 84 | this.loadingSpinnerTask.perform(), 85 | ]); 86 | }; 87 | } 88 | `, 89 | ` 90 | import { all } from 'ember-concurrency'; 91 | 92 | export default class extends Component { 93 | @restartableTask *submitTask() { 94 | yield all([ 95 | this.saveTask.perform(), 96 | this.loadingSpinnerTask.perform(), 97 | ]); 98 | }; 99 | } 100 | `, 101 | ` 102 | import { all } from 'ember-concurrency'; 103 | 104 | export default class extends Component { 105 | @dropTask *submitTask() { 106 | yield all([ 107 | this.saveTask.perform(), 108 | this.loadingSpinnerTask.perform(), 109 | ]); 110 | }; 111 | } 112 | `, 113 | ` 114 | import { all } from 'ember-concurrency'; 115 | 116 | export default class extends Component { 117 | @keepLatestTask *submitTask() { 118 | yield all([ 119 | this.saveTask.perform(), 120 | this.loadingSpinnerTask.perform(), 121 | ]); 122 | }; 123 | } 124 | `, 125 | ` 126 | import { all } from 'ember-concurrency'; 127 | 128 | export default class extends Component { 129 | @enqueueAsk *submitTask() { 130 | yield all([ 131 | this.saveTask.perform(), 132 | this.loadingSpinnerTask.perform(), 133 | ]); 134 | }; 135 | } 136 | `, 137 | ], 138 | 139 | invalid: [ 140 | { 141 | code: ` 142 | export default Component.extend({ 143 | submitTask: task(function*() { 144 | yield Promise.all([ 145 | this.saveTask.perform(), 146 | this.loadingSpinnerTask.perform(), 147 | ]); 148 | }) 149 | }); 150 | `, 151 | errors: [ 152 | { 153 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 154 | line: 4, 155 | column: 19, 156 | endLine: 7, 157 | endColumn: 15, 158 | }, 159 | ], 160 | }, 161 | { 162 | code: ` 163 | export default class extends Component { 164 | @task(function*() { 165 | yield Promise.all([ 166 | this.saveTask.perform(), 167 | this.loadingSpinnerTask.perform(), 168 | ]); 169 | }) 170 | submitTask; 171 | } 172 | `, 173 | errors: [ 174 | { 175 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 176 | line: 4, 177 | column: 19, 178 | endLine: 7, 179 | endColumn: 15, 180 | }, 181 | ], 182 | }, 183 | { 184 | code: ` 185 | export default class extends Component { 186 | @task(function*() { 187 | yield Promise.race([ 188 | this.saveTask.perform(), 189 | this.loadingSpinnerTask.perform(), 190 | ]); 191 | }) 192 | submitTask; 193 | } 194 | `, 195 | errors: [ 196 | { 197 | message: "Use `import { race } from 'ember-concurrency';` instead of `Promise.race()`", 198 | line: 4, 199 | column: 19, 200 | endLine: 7, 201 | endColumn: 15, 202 | }, 203 | ], 204 | }, 205 | { 206 | code: ` 207 | export default class extends Component { 208 | @task(function*() { 209 | yield Promise.allSettled([ 210 | this.saveTask.perform(), 211 | this.loadingSpinnerTask.perform(), 212 | ]); 213 | }) 214 | submitTask; 215 | } 216 | `, 217 | errors: [ 218 | { 219 | message: 220 | "Use `import { allSettled } from 'ember-concurrency';` instead of `Promise.allSettled()`", 221 | line: 4, 222 | column: 19, 223 | endLine: 7, 224 | endColumn: 15, 225 | }, 226 | ], 227 | }, 228 | { 229 | code: ` 230 | export default class extends Component { 231 | @task *submitTask() { 232 | yield Promise.all([ 233 | this.saveTask.perform(), 234 | this.loadingSpinnerTask.perform(), 235 | ]); 236 | } 237 | } 238 | `, 239 | errors: [ 240 | { 241 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 242 | line: 4, 243 | column: 19, 244 | endLine: 7, 245 | endColumn: 15, 246 | }, 247 | ], 248 | }, 249 | { 250 | code: ` 251 | export default class extends Component { 252 | @restartableTask *submitTask() { 253 | yield Promise.all([ 254 | this.saveTask.perform(), 255 | this.loadingSpinnerTask.perform(), 256 | ]); 257 | } 258 | } 259 | `, 260 | errors: [ 261 | { 262 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 263 | line: 4, 264 | column: 19, 265 | endLine: 7, 266 | endColumn: 15, 267 | }, 268 | ], 269 | }, 270 | { 271 | code: ` 272 | export default class extends Component { 273 | @dropTask *submitTask() { 274 | yield Promise.all([ 275 | this.saveTask.perform(), 276 | this.loadingSpinnerTask.perform(), 277 | ]); 278 | } 279 | } 280 | `, 281 | errors: [ 282 | { 283 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 284 | line: 4, 285 | column: 19, 286 | endLine: 7, 287 | endColumn: 15, 288 | }, 289 | ], 290 | }, 291 | { 292 | code: ` 293 | export default class extends Component { 294 | @keepLatestTask *submitTask() { 295 | yield Promise.all([ 296 | this.saveTask.perform(), 297 | this.loadingSpinnerTask.perform(), 298 | ]); 299 | } 300 | } 301 | `, 302 | errors: [ 303 | { 304 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 305 | line: 4, 306 | column: 19, 307 | endLine: 7, 308 | endColumn: 15, 309 | }, 310 | ], 311 | }, 312 | { 313 | code: ` 314 | export default class extends Component { 315 | @enqueueTask *submitTask() { 316 | yield Promise.all([ 317 | this.saveTask.perform(), 318 | this.loadingSpinnerTask.perform(), 319 | ]); 320 | } 321 | } 322 | `, 323 | errors: [ 324 | { 325 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 326 | line: 4, 327 | column: 19, 328 | endLine: 7, 329 | endColumn: 15, 330 | }, 331 | ], 332 | }, 333 | { 334 | code: ` 335 | export default class extends Component { 336 | @task({ maxConcurrency: 3, }) *submitTask() { 337 | yield Promise.all([ 338 | this.saveTask.perform(), 339 | this.loadingSpinnerTask.perform(), 340 | ]); 341 | } 342 | } 343 | `, 344 | errors: [ 345 | { 346 | message: "Use `import { all } from 'ember-concurrency';` instead of `Promise.all()`", 347 | line: 4, 348 | column: 19, 349 | endLine: 7, 350 | endColumn: 15, 351 | }, 352 | ], 353 | }, 354 | ], 355 | }); 356 | -------------------------------------------------------------------------------- /rules/no-perform-without-catch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | create(context) { 5 | return { 6 | CallExpression(node) { 7 | let { callee } = node; 8 | if (callee.type !== 'MemberExpression') return; 9 | 10 | let { property } = callee; 11 | if (property.type !== 'Identifier') return; 12 | if (property.name !== 'perform') return; 13 | 14 | let effectiveParent = getParentSkippingThen(node); 15 | if (effectiveParent.type !== 'ExpressionStatement') return; 16 | 17 | context.report({ 18 | node: property, 19 | message: 'Unhandled promise error from `perform()` call', 20 | }); 21 | }, 22 | }; 23 | }, 24 | }; 25 | 26 | function getParentSkippingThen(node) { 27 | let { parent } = node; 28 | if (!parent) return parent; 29 | if (parent.type !== 'MemberExpression') return parent; 30 | 31 | let { property, parent: greatParent } = parent; 32 | if (property.type !== 'Identifier') return parent; 33 | if (property.name !== 'then') return parent; 34 | 35 | if (!greatParent) return parent; 36 | if (greatParent.type !== 'CallExpression') return parent; 37 | 38 | return getParentSkippingThen(greatParent); 39 | } 40 | -------------------------------------------------------------------------------- /rules/no-perform-without-catch.md: -------------------------------------------------------------------------------- 1 | # no-perform-without-catch 2 | 3 | Similar to [catch-or-return], we want to make sure that any 4 | `this.someTask.perform()` calls have either a `.catch(...)` attached to it, they 5 | are returned from a function, or they are used with `yield` or `await`. 6 | 7 | The reason for this is that without explicit error handling on the `perform()` 8 | calls you will up with "Unhandled promise error" issues if you use error 9 | reporting services like [Sentry]. 10 | 11 | [catch-or-return]: https://github.com/xjamundx/eslint-plugin-promise/blob/master/docs/rules/catch-or-return.md 12 | [Sentry]: https://sentry.io 13 | 14 | ## Examples 15 | 16 | This rule **forbids** the following: 17 | 18 | ```js 19 | this.submitTask.perform(); 20 | ``` 21 | 22 | This rule **allows** the following: 23 | 24 | ```js 25 | this.submitTask.perform().catch(error => { ... }); 26 | ``` 27 | 28 | ```js 29 | try { 30 | await this.submitTask.perform(); 31 | } catch (error) { 32 | ... 33 | } 34 | ``` 35 | 36 | ```js 37 | return this.submitTask.perform(); 38 | ``` 39 | -------------------------------------------------------------------------------- /rules/no-perform-without-catch.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require('eslint'); 2 | 3 | const rule = require('./no-perform-without-catch'); 4 | 5 | let ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }, 10 | }); 11 | 12 | ruleTester.run('no-perform-without-catch', rule, { 13 | valid: [ 14 | `function foo() { this.submitTask.perform().catch(error => {}); }`, 15 | `function foo() { this.submitTask.perform().then().then().catch(error => {}); }`, 16 | `function foo() { return this.submitTask.perform(); }`, 17 | `function foo() { return this.submitTask.perform().then().then(); }`, 18 | `async function foo() { await this.submitTask.perform(); }`, 19 | `async function foo() { await this.submitTask.perform().then().then(); }`, 20 | `function* foo() { yield this.submitTask.perform(); }`, 21 | `function* foo() { yield this.submitTask.perform().then().then(); }`, 22 | `function foo() { let promise = this.submitTask.perform(); }`, 23 | `function foo() { let promise = this.submitTask.perform().then().then(); }`, 24 | `perform()`, 25 | `foo['bar']()`, 26 | ], 27 | 28 | invalid: [ 29 | { 30 | code: `function foo() { this.submitTask.perform(); }`, 31 | errors: [ 32 | { message: 'Unhandled promise error from `perform()` call', column: 34, endColumn: 41 }, 33 | ], 34 | }, 35 | { 36 | code: `function foo() { this.submitTask.perform().then().then(); }`, 37 | errors: [ 38 | { message: 'Unhandled promise error from `perform()` call', column: 34, endColumn: 41 }, 39 | ], 40 | }, 41 | ], 42 | }); 43 | -------------------------------------------------------------------------------- /rules/require-task-name-suffix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { hasTaskDecorator, hasTaskCallExpression } = require('../utils/utils'); 4 | 5 | module.exports = { 6 | create(context) { 7 | return { 8 | Property(node) { 9 | if (node.method || node.shorthand || node.computed) return; 10 | 11 | let { key, value } = node; 12 | if (key.type !== 'Identifier' || key.name.endsWith('Task')) return; 13 | 14 | if (hasTaskCallExpression(value)) { 15 | context.report({ 16 | node: node.key, 17 | message: 'Task names should end with `Task`', 18 | }); 19 | } 20 | }, 21 | MethodDefinition(node) { 22 | let { key, value, decorators } = node; 23 | if (key.type !== 'Identifier' || key.name.endsWith('Task')) return; 24 | if (!decorators) return; 25 | if (!value) return; 26 | if (!value.generator) return; 27 | 28 | if (hasTaskDecorator(node)) { 29 | context.report({ 30 | node: node.key, 31 | message: 'Task names should end with `Task`', 32 | }); 33 | } 34 | }, 35 | ClassProperty(node) { 36 | if (node.static || node.computed) return; 37 | 38 | let { key, value, decorators } = node; 39 | if (key.type !== 'Identifier' || key.name.endsWith('Task')) return; 40 | if (value !== null) return; 41 | if (!decorators) return; 42 | 43 | for (let decorator of node.decorators) { 44 | if (hasTaskCallExpression(decorator.expression)) { 45 | context.report({ 46 | node: node.key, 47 | message: 'Task names should end with `Task`', 48 | }); 49 | } 50 | } 51 | }, 52 | }; 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /rules/require-task-name-suffix.md: -------------------------------------------------------------------------------- 1 | # require-task-name-suffix 2 | 3 | This rule ensures that the names of all `ember-concurrency` tasks in the app 4 | end with `Task` to distinguish them from regular methods, actions or other 5 | properties on the classes. 6 | 7 | 8 | ## Examples 9 | 10 | This rule **forbids** the following: 11 | 12 | ```js 13 | export default Component.extend({ 14 | submit: task(function*() { 15 | //... 16 | }), 17 | }) 18 | ``` 19 | 20 | ```js 21 | export default class extends Component { 22 | @task *submit() { 23 | //... 24 | }; 25 | } 26 | ``` 27 | 28 | This rule **allows** the following: 29 | 30 | ```js 31 | export default Component.extend({ 32 | submitTask: task(function*() { 33 | //... 34 | }), 35 | }) 36 | ``` 37 | 38 | ```js 39 | export default class extends Component { 40 | @task *submitTask() { 41 | //... 42 | }; 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /rules/require-task-name-suffix.test.js: -------------------------------------------------------------------------------- 1 | const { RuleTester } = require('eslint'); 2 | 3 | const rule = require('./require-task-name-suffix'); 4 | 5 | let VALID = [ 6 | `export default Component.extend({ something: task() });`, 7 | `export default Component.extend({ submitTask: task(function*() {}) });`, 8 | `export default Component.extend({ submitTask: task(function*() {}).drop() });`, 9 | `export default Component.extend({ foo: 42 });`, 10 | `export default Component.extend({ [foo]: 42 });`, 11 | `export default Component.extend({ foo() {} });`, 12 | `export default Component.extend({ foo: computed() });`, 13 | ]; 14 | 15 | let INVALID = [ 16 | { 17 | code: `export default Component.extend({ submit: task(function*() {}) });`, 18 | errors: [{ message: 'Task names should end with `Task`', column: 35 }], 19 | }, 20 | { 21 | code: `export default Component.extend({ submit: task(function*() {}).drop() });`, 22 | errors: [{ message: 'Task names should end with `Task`', column: 35 }], 23 | }, 24 | ]; 25 | 26 | let VALID_BABEL = [ 27 | `export default class extends Component { @task something; }`, 28 | `export default class extends Component { @task *submitTask() { }; }`, 29 | `export default class extends Component { @restartableTask *submitTask() { }; }`, 30 | `export default class extends Component { @dropTask *submitTask() { }; }`, 31 | `export default class extends Component { @keepLatestTask *submitTask() { }; }`, 32 | `export default class extends Component { @enqueueTask *submitTask() { }; }`, 33 | `export default class extends Component { @task(function*() {}) submitTask; }`, 34 | `export default class extends Component { @(task(function*() {})) submitTask; }`, 35 | `export default class extends Component { @(task(function*() {}).drop()) submitTask; }`, 36 | `export default class extends Component { foo = 42 }`, 37 | `export default class extends Component { [foo] = 42 }`, 38 | `export default class extends Component { foo() {} }`, 39 | `export default class extends Component { @computed() foo }`, 40 | ]; 41 | 42 | let INVALID_BABEL = [ 43 | { 44 | code: `export default class extends Component { @task(function*() {}) submit; }`, 45 | errors: [{ message: 'Task names should end with `Task`', column: 64 }], 46 | }, 47 | { 48 | code: `export default class extends Component { @(task(function*() {})) submit; }`, 49 | errors: [{ message: 'Task names should end with `Task`', column: 66 }], 50 | }, 51 | { 52 | code: `export default class extends Component { @(task(function*() {}).drop()) submit; }`, 53 | errors: [{ message: 'Task names should end with `Task`', column: 73 }], 54 | }, 55 | { 56 | code: `export default class extends Component { @task *submit() { } }`, 57 | errors: [{ message: 'Task names should end with `Task`', column: 49 }], 58 | }, 59 | { 60 | code: `export default class extends Component { @restartableTask *submit() { } }`, 61 | errors: [{ message: 'Task names should end with `Task`', column: 60 }], 62 | }, 63 | { 64 | code: `export default class extends Component { @dropTask *submit() { } }`, 65 | errors: [{ message: 'Task names should end with `Task`', column: 53 }], 66 | }, 67 | { 68 | code: `export default class extends Component { @keepLatestTask *submit() { } }`, 69 | errors: [{ message: 'Task names should end with `Task`', column: 59 }], 70 | }, 71 | { 72 | code: `export default class extends Component { @enqueueTask *submit() { } }`, 73 | errors: [{ message: 'Task names should end with `Task`', column: 56 }], 74 | }, 75 | { 76 | code: `export default class extends Component { @task({ maxConcurrency: 3, }) *submit() { } }`, 77 | errors: [{ message: 'Task names should end with `Task`', column: 73 }], 78 | }, 79 | ]; 80 | 81 | let ruleTester = new RuleTester({ 82 | parserOptions: { 83 | ecmaVersion: 2018, 84 | sourceType: 'module', 85 | }, 86 | }); 87 | 88 | ruleTester.run('require-task-name-suffix', rule, { 89 | valid: VALID, 90 | invalid: INVALID, 91 | }); 92 | 93 | let babelRuleTester = new RuleTester({ 94 | parser: require.resolve('babel-eslint'), 95 | parserOptions: { 96 | ecmaVersion: 2018, 97 | sourceType: 'module', 98 | }, 99 | }); 100 | 101 | babelRuleTester.run('require-task-name-suffix', rule, { 102 | valid: [...VALID, ...VALID_BABEL], 103 | invalid: [...INVALID, ...INVALID_BABEL], 104 | }); 105 | -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TASK_TYPES = ['task', 'restartableTask', 'dropTask', 'keepLatestTask', 'enqueueTask']; 4 | 5 | module.exports = { hasTaskDecorator, hasTaskCallExpression }; 6 | 7 | function hasTaskDecorator(node) { 8 | if (!node.decorators) return false; 9 | 10 | return node.decorators.some(decorator => { 11 | let { expression } = decorator; 12 | if (!expression) return false; 13 | if (expression.type === 'Identifier' && TASK_TYPES.includes(expression.name)) return true; 14 | return ( 15 | expression.type === 'CallExpression' && 16 | expression.callee && 17 | expression.callee.type === 'Identifier' && 18 | TASK_TYPES.includes(expression.callee.name) 19 | ); 20 | }); 21 | } 22 | 23 | function hasTaskCallExpression(node) { 24 | return Boolean(findTaskCallExpression(node)); 25 | } 26 | 27 | function findTaskCallExpression(node) { 28 | if (isTaskCallExpression(node)) { 29 | return node; 30 | } 31 | 32 | if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') { 33 | return findTaskCallExpression(node.callee.object); 34 | } 35 | } 36 | 37 | function isTaskCallExpression(node) { 38 | return ( 39 | node.type === 'CallExpression' && 40 | node.callee.type === 'Identifier' && 41 | node.callee.name === 'task' && 42 | node.arguments[0] && 43 | node.arguments[0].type === 'FunctionExpression' && 44 | node.arguments[0].generator 45 | ); 46 | } 47 | --------------------------------------------------------------------------------