├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github ├── auto-merge.yml └── workflows │ └── prs.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── guides │ ├── development.md │ ├── integration.md │ └── setup.md ├── media │ └── epm.gif └── rules │ ├── audit-argument-checks.md │ ├── eventmap-params.md │ ├── no-dom-lookup-on-created.md │ ├── no-session.md │ ├── no-template-lifecycle-assignments.md │ ├── no-template-parent-data.md │ ├── no-zero-timeout.md │ ├── prefer-session-equals.md │ ├── prefix-eventmap-selectors.md │ ├── scope-dom-lookups.md │ └── template-names.md ├── lib ├── index.js ├── rules │ ├── audit-argument-checks.js │ ├── eventmap-params.js │ ├── no-dom-lookup-on-created.js │ ├── no-session.js │ ├── no-template-lifecycle-assignments.js │ ├── no-template-parent-data.js │ ├── no-zero-timeout.js │ ├── prefer-session-equals.js │ ├── prefix-eventmap-selectors.js │ ├── scope-dom-lookups.js │ └── template-names.js └── util │ ├── ast │ ├── getPropertyName.js │ ├── index.js │ ├── isFunction.js │ ├── isMeteorCall.js │ ├── isMeteorProp.js │ └── isTemplateProp.js │ ├── environment.js │ ├── executors │ ├── filterExecutorsByAncestors.js │ ├── getExecutors.js │ ├── getExecutorsByEnv.js │ ├── getExecutorsFromTest.js │ ├── invert.js │ ├── isMeteorBlockOnlyTest.js │ └── sets.js │ └── values.js ├── package-lock.json ├── package.json ├── scripts └── new-rule.js └── tests ├── index.js └── lib ├── rules ├── audit-argument-checks.js ├── eventmap-params.js ├── no-dom-lookup-on-created.js ├── no-session.js ├── no-template-lifecycle-assignments.js ├── no-template-parent-data.js ├── no-zero-timeout.js ├── prefer-session-equals.js ├── prefix-eventmap-selectors.js ├── scope-dom-lookups.js └── template-names.js └── util ├── ast ├── getPropertyName.js ├── index.js └── isMeteorCall.js └── executors ├── filterExecutorsByAncestors.js ├── getExecutors.js ├── getExecutorsByEnv.js ├── getExecutorsFromTest.js ├── isMeteorBlockOnlyTest.js └── sets.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | coverage/** 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true # ignore config in main folder 2 | 3 | parserOptions: 4 | ecmaVersion: 6 5 | sourceType: module 6 | 7 | extends: 8 | - prettier 9 | 10 | env: 11 | node: true 12 | mocha: true 13 | 14 | plugins: 15 | - prettier 16 | 17 | rules: 18 | prettier/prettier: ['error', { 'trailingComma': 'es5', 'singleQuote': true }] 19 | semi: ['error', 'always'] 20 | global-require: 0 21 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-auto-merge - https://github.com/bobvanderlinden/probot-auto-merge 2 | 3 | deleteBranchAfterMerge: true 4 | updateBranch: true 5 | mergeMethod: rebase 6 | 7 | minApprovals: 8 | MEMBER: 1 9 | maxRequestedChanges: 10 | COLLABORATOR: 0 11 | blockingLabels: 12 | - WIP 13 | - Blocked 14 | 15 | rules: 16 | - minApprovals: 17 | OWNER: 1 18 | - requiredLabels: 19 | - Automerge 20 | -------------------------------------------------------------------------------- /.github/workflows/prs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: PR 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Test 22 | run: npm run test 23 | - name: Coveralls 24 | run: npm run coveralls 25 | env: 26 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 27 | - name: Release 28 | if: github.ref == 'refs/heads/master' 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | NODE_ENV: production 33 | CI: true 34 | run: npx semantic-release 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | npm-debug.log 4 | .nyc_output 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dominik Ferber 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESLint-plugin-Meteor 2 | 3 | Meteor specific linting rules for ESLint 4 | 5 | [![Build Status][actions-image]][actions-url] 6 | [![Coverage Status][coverage-image]][coverage-url] 7 | [![Dependency Status][deps-image]][deps-url] 8 | 9 | [![Join the chat at https://gitter.im/dferber90/eslint-plugin-meteor][gitter-image]][gitter-url] 10 | [![Maintenance Status][status-image]][status-url] 11 | [![semantic-release][semantic-release-image]][semantic-release] 12 | [![Commitizen friendly][commitizen-image]][commitizen] 13 | 14 | [![License][license-image]][license-url] 15 | [![NPM version][npm-image]][npm-url] 16 | [![NPM downloads][npm-downloads-image]][npm-downloads-url] 17 | 18 | > This repository has been merged into Meteor main repository. You can [find it there](https://github.com/meteor/meteor/tree/devel/npm-packages/eslint-plugin-meteor). 19 | 20 | ![Example](https://raw.githubusercontent.com/dferber90/eslint-plugin-meteor/master/docs/media/epm.gif) 21 | 22 | *This gif shows integration of ESLint-plugin-Meteor into Atom. Find out more in the [integration guide](docs/guides/integration.md).* 23 | 24 | 25 | # Quickstart 26 | ## Installation 27 | 28 | Install [ESLint](https://www.github.com/eslint/eslint) and this plugin either locally or globally. 29 | 30 | ```sh 31 | $ npm install eslint --save-dev 32 | $ npm install eslint-plugin-meteor --save-dev 33 | ``` 34 | 35 | 36 | ## Configuration 37 | 38 | Create an `.eslintrc.json` file with this content at the root of your project: 39 | 40 | ```json 41 | { 42 | "plugins": ["meteor"], 43 | "extends": ["plugin:meteor/recommended"] 44 | } 45 | ``` 46 | 47 | For a more thorough introduction, read the [setup guide](/docs/guides/setup.md). 48 | 49 | An article with detailed setup instructions can be found [here](https://medium.com/@dferber90/linting-meteor-8f229ebc7942). 50 | 51 | # List of supported rules 52 | 53 | ## Best Practices 54 | 55 | * General 56 | * [no-zero-timeout](docs/rules/no-zero-timeout.md): Prevent usage of Meteor.setTimeout with zero delay 57 | * Session 58 | * [no-session](docs/rules/no-session.md): Prevent usage of Session 59 | * [prefer-session-equals](docs/rules/prefer-session-equals.md): Prefer `Session.equals` in conditions 60 | * Security 61 | * [audit-argument-checks](docs/rules/audit-argument-checks.md): Enforce check on all arguments passed to methods and publish functions 62 | * Blaze 63 | * [template-names](docs/rules/template-names.md): Naming convention for templates 64 | * [no-template-lifecycle-assignments](docs/rules/no-template-lifecycle-assignments.md): Prevent deprecated template lifecycle callback assignments 65 | * [eventmap-params](docs/rules/eventmap-params.md): Force consistent event handler parameter names in event maps 66 | * [prefix-eventmap-selectors](docs/rules/prefix-eventmap-selectors.md): Convention for eventmap selectors 67 | * [scope-dom-lookups](docs/rules/scope-dom-lookups.md): Scope DOM lookups to the template instance 68 | * [no-dom-lookup-on-created](docs/rules/no-dom-lookup-on-created.md): Forbid DOM lookups in template creation callback 69 | * [no-template-parent-data](docs/rules/no-template-parent-data.md): Avoid accessing template parent data 70 | 71 | ## Core API 72 | * *currently no rules implemented* 73 | 74 | [Any rule idea is welcome !](https://github.com/dferber90/eslint-plugin-meteor/issues) 75 | 76 | ## Recommended Configuration 77 | 78 | This plugin exports a recommended configuration which enforces good Meteor practices. 79 | The rules enabled in this configuration can be found in [`lib/index.js`](https://github.com/dferber90/eslint-plugin-meteor/blob/master/lib/index.js). 80 | 81 | To enable the recommended configuration use the extends property in your `.eslintrc.json` config file: 82 | 83 | ```json 84 | { 85 | "plugins": [ 86 | "meteor" 87 | ], 88 | "extends": ["eslint:recommended", "plugin:meteor/recommended"] 89 | } 90 | ``` 91 | 92 | You probably also want to enable ESLint to parse ECMAScript 2015 and to support React templates. 93 | 94 | Add the following to your `.eslintrc.json` config file 95 | 96 | ```json 97 | { 98 | "parserOptions": { 99 | "ecmaVersion": 6, 100 | "sourceType": "module", 101 | "ecmaFeatures": { 102 | "jsx": true 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | See [ESLint documentation](http://eslint.org/docs/user-guide/configuring#extending-configuration-files) for more information about extending configuration files. 109 | 110 | ## Limitations 111 | 112 | ESLint-plugin-Meteor is not aware of where files are going to be executed, to keep the plugin simple. 113 | It will not warn when accessing client-only features on the server and vice versa. 114 | 115 | # Contributing 116 | 117 | Read about [set up of the development environment](/docs/guides/development.md). 118 | 119 | # Thanks 120 | 121 | This plugin is inspired by [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react). 122 | 123 | # License 124 | 125 | ESLint-plugin-Meteor is licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php). 126 | 127 | 128 | [gitter-image]: https://img.shields.io/badge/gitter-chat-e10079.svg?style=flat-square 129 | [gitter-url]: https://gitter.im/dferber90/eslint-plugin-meteor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 130 | 131 | [npm-url]: https://npmjs.org/package/eslint-plugin-meteor 132 | [npm-image]: http://img.shields.io/npm/v/eslint-plugin-meteor.svg?style=flat-square 133 | 134 | [npm-downloads-url]: https://npm-stat.com/charts.html?package=eslint-plugin-meteor 135 | [npm-downloads-image]: https://img.shields.io/npm/dm/eslint-plugin-meteor.svg?style=flat-square 136 | 137 | [actions-url]: https://github.com/dferber90/eslint-plugin-meteor/actions?query=workflow%3APR 138 | [actions-image]: https://img.shields.io/github/workflow/status/dferber90/eslint-plugin-meteor/PR?style=flat-square 139 | 140 | [deps-url]: https://david-dm.org/dferber90/eslint-plugin-meteor 141 | [deps-image]: https://img.shields.io/david/dev/dferber90/eslint-plugin-meteor.svg?style=flat-square 142 | 143 | [coverage-url]: https://coveralls.io/github/dferber90/eslint-plugin-meteor?branch=master 144 | [coverage-image]: http://img.shields.io/coveralls/dferber90/eslint-plugin-meteor/master.svg?style=flat-square 145 | 146 | [license-url]: https://github.com/dferber90/eslint-plugin-meteor/blob/master/LICENSE 147 | [license-image]: https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square 148 | 149 | [status-url]: https://github.com/dferber90/eslint-plugin-meteor/pulse 150 | [status-image]: http://img.shields.io/badge/status-maintained-e10079.svg?style=flat-square 151 | 152 | [semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square 153 | [semantic-release]: https://github.com/semantic-release/semantic-release 154 | 155 | [commitizen-image]: https://img.shields.io/badge/commitizen-friendly-e10079.svg?style=flat-square 156 | [commitizen]: http://commitizen.github.io/cz-cli/ 157 | -------------------------------------------------------------------------------- /docs/guides/development.md: -------------------------------------------------------------------------------- 1 | # Setup Development Environment 2 | 3 | This document describes how developers can contribute by adding rules for ESLint-plugin-Meteor. Before implementing a rule, create an issue to discuss the proposed rule. After getting some feedback, you can develop the rule. Every rule must have adequate tests and documentation. Reading the [ESLint developer guide](http://eslint.org/docs/developer-guide/) is a good start. 4 | 5 | Run the following commands to set up ESLint-plugin-Meteor in development mode. 6 | 7 | ```bash 8 | # clone repository 9 | $ git clone git@github.com:dferber90/eslint-plugin-meteor.git 10 | 11 | # install dependencies 12 | $ npm install 13 | ``` 14 | 15 | ## Development Setup 16 | 17 | This plugin runs untranspiled. The source code needs to be compatible with node version 4 and upwards. 18 | 19 | Run `npm run` to see the available scripts for tests, unit-tests and so on. 20 | 21 | ```bash 22 | # run unit-tests only 23 | $ npm run unit-test 24 | 25 | # run linter only 26 | $ npm run lint 27 | 28 | # run unit-tests only 29 | $ npm run unit-test 30 | 31 | # run unit-tests in watch mode 32 | $ npm run unit-test:watch 33 | 34 | # run complete test suite 35 | $ npm test 36 | ``` 37 | 38 | ## Linking 39 | 40 | npm can link packages. This makes version set up for development available in other projects. It enables testing new rules on real projects. To be able to link this package to another project, that one has to be [set up correctly first](/docs/guides/setup.md). 41 | 42 | ```bash 43 | # Make this package available globally 44 | # by running this command from the root of this package 45 | $ npm link 46 | 47 | # In a project using this plugin, install the linked version 48 | $ npm link eslint-plugin-meteor 49 | ``` 50 | 51 | Read more about linking [here](https://docs.npmjs.com/cli/link). 52 | 53 | ## Creating rules 54 | 55 | Creating rules for ESLint-plugin-Meteor is best done by using the scaffolding tool. 56 | 57 | ```bash 58 | $ npm run rule 59 | ``` 60 | 61 | This will scaffold all required files for the new rule. Add the implementation, tests and description of your rule to these files. 62 | 63 | After implementation, the rule has to be exported from `lib/index.js`. 64 | Recommended options for the rule should be set as well (also in `lib/index.js`). 65 | 66 | ## Give back 67 | 68 | After making sure all tests pass and the test-coverage is at 100%, please send a PR to [dferber90/eslint-plugin-meteor](https://github.com/dferber90/eslint-plugin-meteor). 69 | Git commits messages must follow the [conventional changelog](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines) format. This is important as we're releasing automatically and the next version is determined upon the commit messages. 70 | 71 | ## Essential Development Resources 72 | 73 | These specs and tools help enormously when developing new rules. 74 | 75 | * [ESTree Spec](https://github.com/estree/estree/blob/master/spec.md) 76 | * [Espree Parser](http://eslint.org/parser/) 77 | * [JS AST Explorer](http://astexplorer.net/) 78 | -------------------------------------------------------------------------------- /docs/guides/integration.md: -------------------------------------------------------------------------------- 1 | # Integration Guide 2 | 3 | *Make sure ESLint and ESLint-plugin-Meteor are set up correctly before looking into integrations. This is explained in the [setup guide](/docs/guides/setup.md).* 4 | 5 | This document describes how to set up ESLint for different IDEs and build processes. 6 | 7 | 8 | ## Atom Editor 9 | 10 | Install these packages in Atom: 11 | 12 | - https://atom.io/packages/linter 13 | - https://atom.io/packages/linter-eslint 14 | -------------------------------------------------------------------------------- /docs/guides/setup.md: -------------------------------------------------------------------------------- 1 | # Setup Guide 2 | 3 | This document describes how to set up ESLint and ESLint-plugin-Meteor in Meteor projects. 4 | _It must have steps for Meteor projects before 1.3 and with 1.3._ 5 | _It should further show how to use only selected rules (or link to the page of the ESLint documentation)_ 6 | 7 | This guide assumes you have [npm](https://www.npmjs.com/) installed. 8 | 9 | ## Setup 10 | 11 | If you don't have a `package.json` at the root of your Meteor project, create one with `npm init --yes`. Next, add `private: true` to your package (to avoid some warnings and prevent publishing your project to npm's registry accidentally). 12 | 13 | Now, install ESLint and ESLint-plugin-Meteor as development dependencies: 14 | 15 | ```bash 16 | $ npm install eslint eslint-plugin-meteor --save-dev 17 | ``` 18 | 19 | Next, an ESLint configuration needs to be created to enable some rules. 20 | Create a file called `.eslintrc.json` at the root of your project. 21 | 22 | A minimal configuration should look like this: 23 | 24 | ```json 25 | { 26 | "parserOptions": { 27 | "ecmaVersion": 6, 28 | "sourceType": "module", 29 | "ecmaFeatures": { 30 | "jsx": true 31 | } 32 | }, 33 | "plugins": ["meteor"], 34 | "extends": ["plugin:meteor/recommended"] 35 | } 36 | ``` 37 | 38 | And that's it 🎉! 39 | 40 | More information on setting up ESLint can be found [here](http://eslint.org/docs/user-guide/configuring). 41 | 42 | An article with detailed setup instructions specifically for Meteor projects can be found [here](https://medium.com/@dferber90/linting-meteor-8f229ebc7942). 43 | 44 | ## Tips 45 | 46 | Here are some more tips to further improve the setup. 47 | 48 | ### Add environments 49 | 50 | An environment tells ESLint about defined globals. 51 | Since Meteor code can run in the browser and on the server, it's wise to add `browser` and `node`. As Meteor supports ES2015, `es6` should be added as well. And of course the `meteor` environment itself. Since Meteor 1.3 applications can use modules, they should be enabled for ESLint as well. 52 | 53 | ```js 54 | { 55 | /* ... */ 56 | "parserOptions": { 57 | "ecmaVersion": 6, 58 | "sourceType": "module" 59 | }, 60 | "env": { 61 | "es6": true, 62 | "browser": true, 63 | "node": true, 64 | "meteor": true 65 | }, 66 | /* ... */ 67 | } 68 | ``` 69 | 70 | ### Collections and globals 71 | 72 | ESLint needs to know about globals defined in your application. 73 | Add the globals key to `.eslintrc.json`: 74 | 75 | ```js 76 | { 77 | /* ... */ 78 | "globals": { 79 | "MyCollection": true, 80 | "moment": false 81 | }, 82 | /* ... */ 83 | } 84 | ``` 85 | 86 | Here, you can define all globals your application uses. This is also the place to add globals provided through packages from Atmosphere. The boolean values tell ESLint whether it is okay for your application code to overwrite these globals (`true`) or not (`false`). 87 | 88 | ### Usage with React 89 | 90 | If you are using React, you should: 91 | 92 | * enable JSX syntax (see: [ESLint configuration documentation](http://eslint.org/docs/user-guide/configuring#specifying-parser-options)) 93 | * use ESLint-plugin-React (see: [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react)) 94 | 95 | ## ESLint-config-airbnb 96 | 97 | Use a rule preset like [eslint-config-airbnb](https://www.npmjs.com/package/eslint-config-airbnb). 98 | It has lots of well-thought-out rules. 99 | 100 | ### Using YAML instead 101 | 102 | ESLint supports different formats in which the configuration can be specified. 103 | If `.eslintrc.json` is renamed to `.eslint.yaml` then the full configuration can be written like this: 104 | 105 | ```yaml 106 | --- 107 | root: true 108 | 109 | env: 110 | es6: true 111 | browser: true 112 | node: true 113 | meteor: true 114 | 115 | parserOptions: 116 | ecmaVersion: 6 117 | sourceType: module 118 | ecmaFeatures: 119 | jsx: true 120 | 121 | plugins: 122 | - meteor 123 | 124 | extends: 125 | - airbnb/base 126 | - plugin:meteor/recommended 127 | 128 | globals: 129 | # Collections 130 | MyCollection: true 131 | # .. 132 | 133 | # Packages 134 | moment: false # exported by momentjs:moment 135 | # .. 136 | ``` 137 | -------------------------------------------------------------------------------- /docs/media/epm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/eslint-plugin-meteor/93cdb57bdbfc2018c3f9d565ecd9f8f7795722c6/docs/media/epm.gif -------------------------------------------------------------------------------- /docs/rules/audit-argument-checks.md: -------------------------------------------------------------------------------- 1 | # Enforce check on all arguments passed to methods and publish functions (audit-argument-checks) 2 | 3 | The Meteor package `audit-argument-checks` requires that all arguments in calls to methods and publish functions are `check`ed. 4 | Any method that does not pass each one of its arguments to check will throw an error. 5 | This rule emulates that behavior. Unlike its Meteor counterpart this rule further ensures all `check`'s happen unconditionally. 6 | 7 | 8 | ## Rule Details 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | 14 | Meteor.publish("foo", function (bar) {}) 15 | 16 | Meteor.methods({ 17 | foo: function (bar) {} 18 | }) 19 | 20 | Meteor.methods({ 21 | foo: function (bar) { 22 | if (Math.random() > 0.5) { 23 | check(bar, Match.Any) 24 | } 25 | } 26 | }) 27 | 28 | ``` 29 | 30 | The following patterns are not warnings: 31 | 32 | ```js 33 | 34 | Meteor.publish("foo", function (bar) { 35 | check(bar, Match.Any) 36 | }) 37 | 38 | Meteor.methods({ 39 | foo: function (bar) { 40 | check(bar, Match.Any) 41 | } 42 | }) 43 | 44 | Meteor.methods({ 45 | foo: function (bar) { 46 | var ret; 47 | ret = check(bar, Match.Any) 48 | } 49 | }) 50 | 51 | ``` 52 | 53 | For a check function to be considered "called", it must be called at the 54 | top level of the method or publish function (not e.g. within an `if` block), 55 | either as a lone expression statement or as an assignment statement where the 56 | right-hand side is just the function call (as in the last example above). 57 | 58 | ### Options 59 | 60 | If you define your own functions that call `check`, you can provide a list of 61 | such functions via the configuration `checkEquivalents`. This rule assumes 62 | that these functions effectively check their first argument (an identifier or 63 | an array of identifiers). 64 | 65 | For example, in `.eslintrc.json`, you can specify the following configuration: 66 | 67 | ```json 68 | "meteor/audit-argument-checks": [ 69 | "error", 70 | { 71 | "checkEquivalents": [ 72 | "checkId", 73 | "checkName" 74 | ] 75 | } 76 | ] 77 | ``` 78 | 79 | ## When Not To Use It 80 | 81 | If you are not using Meteor's `check` package, then you should not use this rule. 82 | 83 | ## Further Reading 84 | 85 | * http://docs.meteor.com/#/full/check 86 | * http://docs.meteor.com/#/full/auditargumentchecks 87 | 88 | ## Possible Improvements 89 | 90 | * Emulate behavior of Meteor's `audit-argument-checks` more closely 91 | * Support immediate destructuring of params 92 | -------------------------------------------------------------------------------- /docs/rules/eventmap-params.md: -------------------------------------------------------------------------------- 1 | # Consistent event handler parameters (eventmap-params) 2 | 3 | Force consistent event handler parameters in [event maps](http://docs.meteor.com/#/full/eventmaps) 4 | 5 | 6 | ## Rule Details 7 | 8 | Prevent the use of differently named parameters in event handlers to achieve more consistent code 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | Template.foo.events({ 14 | // 'foo' does not match 'event' 15 | 'submit form': function (foo) {} 16 | }) 17 | 18 | Template.foo.events({ 19 | // 'bar' does not match 'templateInstance' 20 | 'submit form': function (event, bar) {} 21 | }) 22 | 23 | Template.foo.events({ 24 | // neither 'foo' nor 'bar' are correct 25 | 'submit form': function (foo, bar) {} 26 | }) 27 | 28 | ``` 29 | 30 | The following patterns are not warnings: 31 | 32 | ```js 33 | Template.foo.events({ 34 | 'submit form': function (event) {} 35 | }) 36 | 37 | Template.foo.events({ 38 | 'submit form': function (event, templateInstance) {} 39 | }) 40 | 41 | Template.foo.events({ 42 | 'submit form': function ({ target: form }, { data }) {} 43 | }) 44 | 45 | ``` 46 | 47 | ### Options 48 | 49 | #### Parameter names 50 | 51 | You can optionally set the names of the parameters. 52 | You can set the name of the event parameter using `eventParamName` and the name of the template-instance parameter using `templateInstanceParamName`. 53 | Here are examples of how to do this: 54 | 55 | ```js 56 | /* 57 | eslint meteor/eventmap-params: [2, {"eventParamName": "evt"}] 58 | */ 59 | Template.foo.events({ 60 | 'submit form': function (evt) {} 61 | }) 62 | 63 | /* 64 | eslint meteor/eventmap-params: [2, {"templateInstanceParamName": "tmplInst"}] 65 | */ 66 | Template.foo.events({ 67 | 'submit form': function (event, tmplInst) {} 68 | }) 69 | 70 | /* 71 | eslint meteor/eventmap-params: [2, {"eventParamName": "evt", "templateInstanceParamName": "tmplInst"}] 72 | */ 73 | Template.foo.events({ 74 | 'submit form': function (evt, tmplInst) {} 75 | }) 76 | 77 | ``` 78 | 79 | #### Destructuring 80 | 81 | You can optionally forbid destructuring the parameters. 82 | You can set `preventDestructuring` to `"event"`, `"templateInstance"`, or `"both"`, to force no destructuring on the event parameter, template-instance parameter, or both respectively. 83 | 84 | The following patterns are considered problems: 85 | 86 | ```js 87 | /* 88 | eslint meteor/eventmap-params: [2, {"preventDestructuring": "event"}] 89 | */ 90 | Template.foo.events({ 91 | 'submit form': function ({ target: form }, templateInstance) {} 92 | }) 93 | 94 | /* 95 | eslint meteor/eventmap-params: [2, {"preventDestructuring": "templateInstance"}] 96 | */ 97 | Template.foo.events({ 98 | 'submit form': function (event, { data }) {} 99 | }) 100 | 101 | /* 102 | eslint meteor/eventmap-params: [2, {"preventDestructuring": "both"}] 103 | */ 104 | Template.foo.events({ 105 | 'submit form': function (event, { data }) {} 106 | }) 107 | ``` 108 | 109 | The following patterns are not considered problems: 110 | 111 | ```js 112 | /* 113 | eslint meteor/eventmap-params: [2, {"preventDestructuring": "event"}] 114 | */ 115 | Template.foo.events({ 116 | 'submit form': function (event, { data }) {} 117 | }) 118 | 119 | /* 120 | eslint meteor/eventmap-params: [2, {"preventDestructuring": "templateInstance"}] 121 | */ 122 | Template.foo.events({ 123 | 'submit form': function ({ target: form }, templateInstance) {} 124 | }) 125 | ``` 126 | 127 | ## Further Reading 128 | 129 | * http://docs.meteor.com/#/full/eventmaps 130 | -------------------------------------------------------------------------------- /docs/rules/no-dom-lookup-on-created.md: -------------------------------------------------------------------------------- 1 | # Forbid DOM lookup in template creation callback (no-dom-lookup-on-created) 2 | 3 | When the `onCreated` lifecycle callback is called, the template does not yet exist in the DOM. Trying to access its elements is most likely an error. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to prevent accessing a templates elements before they are attached to the DOM. 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | 14 | Template.foo.onCreated(function () { 15 | $('.bar').focus() 16 | }) 17 | 18 | Template.foo.onCreated(function () { 19 | Template.instance().$('.bar').focus() 20 | }) 21 | 22 | ``` 23 | 24 | The following patterns are not warnings: 25 | 26 | ```js 27 | 28 | Template.foo.onCreated(function () { 29 | console.log('hello') 30 | }) 31 | 32 | 33 | Template.foo.onRendered(function () { 34 | $('.bar').focus() 35 | Template.instance().$('.bar').focus() 36 | }) 37 | 38 | // should be a warning, but is too hard to check for statically, 39 | // so the rule ignores it 40 | Template.foo.onCreated(function () { 41 | this.$('.bar').focus() 42 | }) 43 | 44 | ``` 45 | 46 | ## Limitations 47 | The rule can not warn when jQuery is invoked through the context. 48 | 49 | ## Further Reading 50 | 51 | - http://docs.meteor.com/#/full/template_onCreated 52 | -------------------------------------------------------------------------------- /docs/rules/no-session.md: -------------------------------------------------------------------------------- 1 | # Prevent usage of Session (no-session) 2 | 3 | This rule prevents any usage of Session. Session variables live in a global namespace, which is bad practice. [reactive-dict](https://github.com/meteor/meteor/tree/devel/packages/reactive-dict) should be used instead. 4 | 5 | ## Rule Details 6 | 7 | This rule enforces a style without `Session`. 8 | 9 | The following patterns are considered warnings: 10 | 11 | ```js 12 | 13 | Session.set('foo') 14 | Session.get('foo') 15 | Session.all() 16 | Session.clear() 17 | 18 | ``` 19 | 20 | The following patterns are not warnings: 21 | 22 | ```js 23 | 24 | Session = true 25 | console.log(Session) 26 | 27 | ``` 28 | 29 | ## When Not To Use It 30 | 31 | If you are working on a project using few globals then you can disable this rule. 32 | 33 | ## Further Reading 34 | 35 | * https://meteor.hackpad.com/Proposal-Deprecate-Session-in-favor-of-ReactiveDict-0wbRKtE4GZ9 36 | * http://c2.com/cgi/wiki?GlobalVariablesAreBad 37 | -------------------------------------------------------------------------------- /docs/rules/no-template-lifecycle-assignments.md: -------------------------------------------------------------------------------- 1 | # Prevent deprecated template lifecycle callback assignments (no-template-lifecycle-assignments) 2 | 3 | Assigning lifecycle callbacks to template properties has been deprecated in favor of the more robust template lifecycle callback registration functions. 4 | 5 | > Add `onRendered`, `onCreated`, and `onDestroyed` methods to Template. Assignments to `Template.foo.rendered` and so forth are deprecated but are still supported for backwards compatibility. - 6 | > 7 | > Source: [Meteor Release History](https://github.com/meteor/meteor/blob/devel/History.md#blaze-2) 8 | 9 | ## Rule Details 10 | 11 | This rule aims to ensure you are not using deprecated functions to register lifecycle callbacks to templates. 12 | 13 | The following patterns are considered warnings: 14 | 15 | ```js 16 | 17 | Template.foo.created = function { /* .. */ } 18 | Template.foo.rendered = function { /* .. */ } 19 | Template.foo.destroyed = function { /* .. */ } 20 | 21 | Template[bar].created = function { /* .. */ } 22 | Template[bar].rendered = function { /* .. */ } 23 | Template[bar].destroyed = function { /* .. */ } 24 | 25 | 26 | ``` 27 | 28 | The following patterns are not warnings: 29 | 30 | ```js 31 | 32 | Template.foo.onCreated(function { /* .. */ }) 33 | Template.foo.onRendered(function { /* .. */ }) 34 | Template.foo.ondestroyed(function { /* .. */ }) 35 | 36 | Template[foo].onCreated(function { /* .. */ }) 37 | Template[foo].onRendered(function { /* .. */ }) 38 | Template[foo].ondestroyed(function { /* .. */ }) 39 | 40 | ``` 41 | 42 | ## When Not To Use It 43 | 44 | This rule should not be used with Meteor below v1.0.4. 45 | 46 | ## Further Reading 47 | 48 | * https://github.com/meteor/meteor/blob/devel/History.md#v104-2015-mar-17 49 | -------------------------------------------------------------------------------- /docs/rules/no-template-parent-data.md: -------------------------------------------------------------------------------- 1 | # Avoid accessing template parent data (no-template-parent-data) 2 | 3 | When making children aware of their parents data context, they are tightly integrated and hard to reuse. 4 | Changing the parent can lead to unintended errors in the child. 5 | Passing down the properties explicitly avoids this issue. 6 | 7 | 8 | ## Rule Details 9 | 10 | This rule aims to ensure child components are unaware of their parents. 11 | 12 | The following patterns are considered warnings: 13 | 14 | ```js 15 | 16 | Template.parentData() 17 | Template.parentData(0) 18 | Template.parentData(1) 19 | Template.parentData(foo) 20 | 21 | ``` 22 | 23 | The following patterns are not warnings: 24 | 25 | ```js 26 | 27 | Template.currentData() 28 | 29 | ``` 30 | 31 | ## Further Reading 32 | 33 | - http://docs.meteor.com/#/full/template_parentdata 34 | -------------------------------------------------------------------------------- /docs/rules/no-zero-timeout.md: -------------------------------------------------------------------------------- 1 | # Prevent usage of Meteor.setTimeout with zero delay (no-zero-timeout) 2 | 3 | `Meteor.setTimeout` can be used to defer the execution of a function, but Meteor has a built-in method for deferring called `Meteor.defer`. It is better to use the dedicated method instead of relying on a side-effect of `Meteor.setTimeout`. 4 | 5 | Using `Meteor.defer` is preferred, because it uses native `setImmediate` or `postMessage` methods in case they are available. Otherwise it can will fall back to `setTimeout`. 6 | It's recommended to avoid `setTimeout` because it adds a delay of at least 2ms in Chrome, 10ms in other browsers [[source](http://dbaron.org/log/20100309-faster-timeouts)]. 7 | 8 | ## Rule Details 9 | 10 | This rule aims to encourage the use of `Meteor.defer` by removing all occurrences of `Meteor.setTimeout` with a delay of 0. 11 | 12 | The following patterns are considered warnings: 13 | 14 | ```js 15 | 16 | Meteor.setTimeout(function () {}, 0) 17 | Meteor.setTimeout(function () {}) 18 | Meteor["setTimeout"](function () {}, 0) 19 | 20 | Meteor.setTimeout(foo, 0) 21 | Meteor.setTimeout(foo) 22 | Meteor["setTimeout"](foo, 0) 23 | 24 | ``` 25 | 26 | The following patterns are not warnings: 27 | 28 | ```js 29 | 30 | Meteor.defer(function () {}, 0) 31 | Meteor.setTimeout(function () {}, 100) 32 | 33 | Meteor.defer(foo, 0) 34 | Meteor.setTimeout(foo, 100) 35 | 36 | ``` 37 | 38 | ## Further Reading 39 | 40 | * https://github.com/meteor/meteor/blob/832e6fe44f3635cae060415d6150c0105f2bf0f6/packages/meteor/setimmediate.js#L1-L7 41 | * http://dbaron.org/log/20100309-faster-timeouts 42 | * https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage 43 | * https://developer.mozilla.org/en/docs/Web/API/Window/setImmediate 44 | -------------------------------------------------------------------------------- /docs/rules/prefer-session-equals.md: -------------------------------------------------------------------------------- 1 | # Prefer `Session.equals` in conditions (prefer-session-equals) 2 | 3 | Using `Session.equals('foo', bar)` toggles fewer invalidations compared to `Session.get('foo') === bar`. This rule warns when unnecessary invalidations would be triggered. 4 | 5 | 6 | ## Rule Details 7 | 8 | While the above is only true for scalar types, this rule encourages use of `Session.equals` in all conditionals. 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | if (Session.get("foo")) {/* ... */} 14 | 15 | if (Session.get("foo") == bar) {/* ... */} 16 | 17 | if (Session.get("foo") === bar) {/* ... */} 18 | 19 | Session.get("foo") ? true : false 20 | 21 | Session.get("foo") === bar ? true : false 22 | ``` 23 | 24 | The following patterns are not warnings: 25 | 26 | ```js 27 | if (Session.equals("foo", true)) {/* ... */} 28 | 29 | if (Session.equals("foo", 1)) {/* ... */} 30 | 31 | if (Session.equals("foo", "hello")) {/* ... */} 32 | 33 | if (Session.equals("foo", bar)) {/* ... */} 34 | 35 | if (_.isEqual(Session.get("foo"), otherValue)) {/* ... */} 36 | 37 | Session.equals("foo", true) ? true : false 38 | ``` 39 | 40 | ```js 41 | const foo = Session.get("foo") 42 | if (foo === 'bar') {/* ... */} 43 | ``` 44 | 45 | ## When Not To Use It 46 | 47 | Turn this rule off when you are comparing compound types, e.g. Arrays. 48 | 49 | 50 | ## Further Reading 51 | 52 | - http://docs.meteor.com/#/full/session_equals 53 | 54 | 55 | ## Possible Improvements 56 | 57 | * Track which variables were set through `Session.get` and warn when they are used in conditions 58 | -------------------------------------------------------------------------------- /docs/rules/prefix-eventmap-selectors.md: -------------------------------------------------------------------------------- 1 | # Convention for eventmap selectors (prefix-eventmap-selectors) 2 | 3 | > When you are setting up event maps in your JS files, you need to ‘select’ the element in the template that the event attaches to. Rather than using the same CSS class names that are used to style the elements, it’s better practice to use classnames that are specifically added for those event maps. A reasonable convention is a class starting with *js-* to indicate it is used by the JavaScript. - [source](http://guide.meteor.com/blaze.html#js-selectors-for-events) 4 | 5 | This rule enforces that convention. 6 | 7 | 8 | ## Rule Details 9 | 10 | This rule aims to ensure all classes with attached event listeners have the same prefix, so they are distinguishable from classes used for styling. 11 | 12 | 13 | ### Options 14 | 15 | This rule takes two arguments: 16 | - the prefix css classes must use, defaults to `js-`. 17 | - the mode in which to run the rule, can be one of the following options 18 | - `relaxed` (default): events can be assigned through any selectors, but class selectors must be prefixed 19 | - `strict`: events can only be assigned to prefixed class selectors 20 | 21 | #### relaxed 22 | 23 | Examples of **incorrect** code for the default `"relaxed"` mode: 24 | 25 | ```js 26 | /*eslint prefix-eventmap-selectors: [2, "js-", "relaxed"]*/ 27 | 28 | Template.foo.events({ 29 | 'click .foo': function () {} 30 | }) 31 | 32 | ``` 33 | 34 | Examples of **correct** code for the default `"relaxed"` mode: 35 | 36 | ```js 37 | /*eslint prefix-eventmap-selectors: [2, "js-", "relaxed"]*/ 38 | 39 | Template.foo.events({ 40 | 'click .js-foo': function () {}, 41 | 'blur .js-bar': function () {}, 42 | 'click #foo': function () {}, 43 | 'click [data-foo="bar"]': function () {}, 44 | 'click input': function () {}, 45 | 'click': function () {}, 46 | }) 47 | 48 | ``` 49 | 50 | #### strict 51 | 52 | Examples of **incorrect** code for the `"strict"` mode: 53 | 54 | ```js 55 | /*eslint prefix-eventmap-selectors: [2, "js-", "strict"]*/ 56 | 57 | Template.foo.events({ 58 | 'click .foo': function () {}, 59 | 'click #foo': function () {}, 60 | 'click input': function () {}, 61 | 'click': function () {}, 62 | 'click [data-foo="bar"]': function () {}, 63 | }) 64 | 65 | ``` 66 | 67 | Examples of **correct** code for the default `"relaxed"` mode: 68 | 69 | ```js 70 | /*eslint prefix-eventmap-selectors: [2, "js-", "strict"]*/ 71 | 72 | Template.foo.events({ 73 | 'click .js-foo': function () {} 74 | }) 75 | 76 | ``` 77 | 78 | ## When Not To Use It 79 | 80 | This rule can be disabled if you are not using Blaze. 81 | 82 | ## Possible Improvements 83 | 84 | - forbid nested selectors `.js-foo .bar`, `.js-foo.bar`, `.js-foo#bar`, `#bar.js-foo`, `.js-foo + .bar` 85 | - enable switching on/off errors for selection by attribute, nesting, plain (no selector), .. 86 | 87 | ## Further Reading 88 | 89 | - http://guide.meteor.com/blaze.html#js-selectors-for-events 90 | -------------------------------------------------------------------------------- /docs/rules/scope-dom-lookups.md: -------------------------------------------------------------------------------- 1 | # Scope DOM lookups to the template instance (scope-dom-lookups) 2 | 3 | > It’s a bad idea to look up things directly in the DOM with jQuery’s global `$()`. It’s easy to select some element on the page that has nothing to do with the current component. Also, it limits your options on rendering outside of the main document. - [source](http://guide.meteor.com/blaze.html#scope-dom-lookups-to-instance) 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to ensure DOM lookups are scoped to the template instance to improve performance and to reduce accidental side-effects. 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | 14 | Template.foo.onRendered(function () { 15 | $('.bar').focus() 16 | }) 17 | 18 | Template.foo.onRendered(function () { 19 | const $bar = $('.bar') 20 | // .. 21 | }) 22 | 23 | Template.foo.events({ 24 | 'click .bar': function (event, instance) { 25 | $('.baz').focus() 26 | } 27 | }) 28 | 29 | Template.foo.helpers({ 30 | 'bar': function () { 31 | $('.baz').focus() 32 | } 33 | }) 34 | 35 | Template.foo.onDestroyed(function () { 36 | $('.bar').focus() 37 | }) 38 | 39 | Template.foo.onRendered(function () { 40 | jQuery('.bar').focus() 41 | }) 42 | 43 | ``` 44 | 45 | The following patterns are not warnings: 46 | 47 | ```js 48 | 49 | Template.foo.onRendered(function () { 50 | this.$('.bar').focus() 51 | }) 52 | 53 | Template.foo.onRendered(function () { 54 | Template.instance().$('.bar').focus() 55 | }) 56 | 57 | Template.foo.events({ 58 | 'click .bar': function (event, instance) { 59 | instance.$('.baz').focus() 60 | } 61 | }) 62 | 63 | ``` 64 | 65 | ## When Not To Use It 66 | 67 | Disable this rule for specific lines if something outside of the template needs to be looked up and there is no way around it. 68 | 69 | ## Further Reading 70 | 71 | - http://guide.meteor.com/blaze.html#scope-dom-lookups-to-instance 72 | -------------------------------------------------------------------------------- /docs/rules/template-names.md: -------------------------------------------------------------------------------- 1 | # Force a naming convention for templates (template-names) 2 | 3 | When it comes to naming templates there are multiple naming conventions available. Enforce one of them with this rule. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to enforce one naming convention for template names is used consistently. 9 | It does this by checking references to the template from the JavaScript code. 10 | 11 | It offers three different naming conventions, one of which can be chosen through the rule options. 12 | 13 | The following patterns are considered warnings: 14 | 15 | ```js 16 | 17 | /*eslint meteor/template-names: [2, "camel-case"]*/ 18 | Template.foo_bar.onCreated 19 | Template.foo_bar.onRendered 20 | Template.foo_bar.onDestroyed 21 | Template.foo_bar.events 22 | Template.foo_bar.helpers 23 | 24 | Template.foo_bar.onCreated() 25 | /* .. */ 26 | 27 | Template.FooBar.onCreated 28 | /* .. */ 29 | 30 | ``` 31 | 32 | The following patterns are not warnings: 33 | 34 | ```js 35 | 36 | /*eslint meteor/template-names: [2, "camel-case"]*/ 37 | Template.fooBar.onCreated 38 | Template.fooBar.onRendered 39 | Template.fooBar.onDestroyed 40 | Template.fooBar.events 41 | Template.fooBar.helpers 42 | 43 | /*eslint meteor/template-names: [2, "pascal-case"]*/ 44 | Template.FooBar.onCreated 45 | /* .. */ 46 | 47 | /*eslint meteor/template-names: [2, "snake-case"]*/ 48 | Template.foo.onCreated 49 | Template.foo_bar.onCreated 50 | 51 | ``` 52 | 53 | ### Options 54 | 55 | This rule accepts a single options argument with the following defaults: 56 | 57 | ```json 58 | { 59 | "rules": { 60 | "template-names": [2, "camel-case"] 61 | } 62 | } 63 | ``` 64 | 65 | The second argument can have the following values: 66 | - `camel-case` 67 | - `pascal-case` 68 | - `snake-case` 69 | 70 | ## Limitations 71 | 72 | This rule can not warn for templates which are never referenced in JavaScript. 73 | 74 | ## When Not To Use It 75 | 76 | If you are not using Blaze templates, it is okay to turn this rule off. 77 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const allRules = { 2 | 'audit-argument-checks': require('./rules/audit-argument-checks'), 3 | 'no-session': require('./rules/no-session'), 4 | 'no-template-lifecycle-assignments': require('./rules/no-template-lifecycle-assignments'), 5 | 'no-zero-timeout': require('./rules/no-zero-timeout'), 6 | 'eventmap-params': require('./rules/eventmap-params'), 7 | 'prefix-eventmap-selectors': require('./rules/prefix-eventmap-selectors'), 8 | 'prefer-session-equals': require('./rules/prefer-session-equals'), 9 | 'template-names': require('./rules/template-names'), 10 | 'scope-dom-lookups': require('./rules/scope-dom-lookups'), 11 | 'no-dom-lookup-on-created': require('./rules/no-dom-lookup-on-created'), 12 | 'no-template-parent-data': require('./rules/no-template-parent-data'), 13 | }; 14 | 15 | module.exports = { 16 | rules: allRules, 17 | configs: { 18 | recommended: { 19 | parserOptions: { 20 | ecmaVersion: 6, 21 | sourceType: 'module', 22 | ecmaFeatures: { jsx: true }, 23 | }, 24 | plugins: ['meteor'], 25 | rules: { 26 | 'meteor/audit-argument-checks': 2, 27 | 'meteor/no-session': 2, 28 | 'meteor/no-template-lifecycle-assignments': 2, 29 | 'meteor/no-zero-timeout': 2, 30 | 'meteor/eventmap-params': 2, 31 | 'meteor/prefix-eventmap-selectors': 0, 32 | 'meteor/prefer-session-equals': 0, 33 | 'meteor/template-names': 2, 34 | 'meteor/scope-dom-lookups': 0, 35 | 'meteor/no-dom-lookup-on-created': 0, 36 | 'meteor/no-template-parent-data': 0, 37 | }, 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /lib/rules/audit-argument-checks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce check on all arguments passed to methods and publish functions 3 | * @author Dominik Ferber 4 | */ 5 | 6 | const { isMeteorCall, isFunction } = require('../util/ast'); 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Rule Definition 10 | // ----------------------------------------------------------------------------- 11 | 12 | module.exports = { 13 | meta: { 14 | schema: [ 15 | { 16 | type: 'object', 17 | properties: { 18 | checkEquivalents: { 19 | type: 'array', 20 | items: { 21 | type: 'string', 22 | minLength: 1, 23 | }, 24 | }, 25 | }, 26 | additionalProperties: false, 27 | }, 28 | ], 29 | }, 30 | create: (context) => { 31 | const options = context.options[0]; 32 | 33 | // --------------------------------------------------------------------------- 34 | // Helpers 35 | // --------------------------------------------------------------------------- 36 | 37 | function isCheck(expression) { 38 | if (expression.callee.name === 'check') { 39 | // Require a second argument for literal check() 40 | return expression.arguments.length > 1; 41 | } else if (options && Array.isArray(options.checkEquivalents)) { 42 | // Allow any number of arguments for checkEquivalents 43 | return options.checkEquivalents.includes(expression.callee.name); 44 | } else { 45 | return false; 46 | } 47 | } 48 | 49 | function auditArgumentChecks(node) { 50 | if (!isFunction(node.type)) { 51 | return; 52 | } 53 | 54 | const checkedParams = []; 55 | function processCheckArgs(args) { 56 | if (args.length > 0 && args[0].type === 'Identifier') { 57 | checkedParams.push(args[0].name); 58 | } 59 | if (args.length > 0 && args[0].type === 'ArrayExpression') { 60 | args[0].elements.forEach((element) => { 61 | if (element.type === 'Identifier') checkedParams.push(element.name); 62 | }); 63 | } 64 | } 65 | 66 | // short-circuit 67 | if (node.params.length === 0) { 68 | return; 69 | } 70 | 71 | if (node.body.type === 'BlockStatement') { 72 | node.body.body.forEach((expression) => { 73 | if ( 74 | expression.type === 'ExpressionStatement' && 75 | expression.expression.type === 'CallExpression' && 76 | expression.expression.callee.type === 'Identifier' && 77 | isCheck(expression.expression) 78 | ) { 79 | processCheckArgs(expression.expression.arguments); 80 | } else if ( 81 | expression.type === 'ExpressionStatement' && 82 | expression.expression.type === 'AssignmentExpression' && 83 | expression.expression.right.type === 'CallExpression' && 84 | expression.expression.right.callee.type === 'Identifier' && 85 | isCheck(expression.expression.right) 86 | ) { 87 | processCheckArgs(expression.expression.right.arguments); 88 | } 89 | }); 90 | } 91 | 92 | node.params.forEach((param) => { 93 | if (param.type === 'Identifier') { 94 | if (checkedParams.indexOf(param.name) === -1) { 95 | context.report(param, `"${param.name}" is not checked`); 96 | } 97 | } else if ( 98 | // check params with default assignments 99 | param.type === 'AssignmentPattern' && 100 | param.left.type === 'Identifier' 101 | ) { 102 | if (checkedParams.indexOf(param.left.name) === -1) { 103 | context.report(param.left, `"${param.left.name}" is not checked`); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | // --------------------------------------------------------------------------- 110 | // Public 111 | // --------------------------------------------------------------------------- 112 | 113 | return { 114 | CallExpression: (node) => { 115 | // publications 116 | if (isMeteorCall(node, 'publish') && node.arguments.length >= 2) { 117 | auditArgumentChecks(node.arguments[1]); 118 | return; 119 | } 120 | 121 | // method 122 | if ( 123 | isMeteorCall(node, 'methods') && 124 | node.arguments.length > 0 && 125 | node.arguments[0].type === 'ObjectExpression' 126 | ) { 127 | node.arguments[0].properties.forEach((property) => { 128 | auditArgumentChecks(property.value); 129 | }); 130 | } 131 | }, 132 | }; 133 | }, 134 | }; 135 | -------------------------------------------------------------------------------- /lib/rules/eventmap-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensures consistent parameter names in blaze event maps 3 | * @author Philipp Sporrer, Dominik Ferber, Rúnar Berg Baugsson Sigríðarson 4 | * @copyright 2016 Philipp Sporrer. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { isFunction, isTemplateProp } = require('../util/ast'); 9 | 10 | // ----------------------------------------------------------------------------- 11 | // Rule Definition 12 | // ----------------------------------------------------------------------------- 13 | 14 | module.exports = { 15 | meta: { 16 | schema: [ 17 | { 18 | type: 'object', 19 | properties: { 20 | eventParamName: { 21 | type: 'string', 22 | }, 23 | templateInstanceParamName: { 24 | type: 'string', 25 | }, 26 | preventDestructuring: { 27 | enum: ['neither', 'both', 'templateInstance', 'event'], 28 | }, 29 | }, 30 | additionalProperties: false, 31 | }, 32 | ], 33 | }, 34 | create: (context) => { 35 | // --------------------------------------------------------------------------- 36 | // Helpers 37 | // --------------------------------------------------------------------------- 38 | 39 | function ensureParamName(param, expectedParamName, preventDestructuring) { 40 | if (param) { 41 | if (param.type === 'ObjectPattern' && preventDestructuring) { 42 | context.report( 43 | param, 44 | `Unexpected destructuring, use name "${expectedParamName}"` 45 | ); 46 | } else if ( 47 | param.type === 'Identifier' && 48 | param.name !== expectedParamName 49 | ) { 50 | context.report( 51 | param, 52 | `Invalid parameter name, use "${expectedParamName}" instead` 53 | ); 54 | } 55 | } 56 | } 57 | 58 | function validateEventDefinition(node) { 59 | const eventHandler = node.value; 60 | if (eventHandler && isFunction(eventHandler.type)) { 61 | const { 62 | eventParamName = 'event', 63 | templateInstanceParamName = 'templateInstance', 64 | preventDestructuring = 'neither', 65 | } = context.options[0] || {}; 66 | 67 | ensureParamName( 68 | eventHandler.params[0], 69 | eventParamName, 70 | preventDestructuring === 'both' || preventDestructuring === 'event' 71 | ); 72 | ensureParamName( 73 | eventHandler.params[1], 74 | templateInstanceParamName, 75 | preventDestructuring === 'both' || 76 | preventDestructuring === 'templateInstance' 77 | ); 78 | } 79 | } 80 | 81 | // --------------------------------------------------------------------------- 82 | // Public 83 | // --------------------------------------------------------------------------- 84 | 85 | return { 86 | CallExpression: (node) => { 87 | if ( 88 | node.arguments.length === 0 || 89 | !isTemplateProp(node.callee, 'events') 90 | ) { 91 | return; 92 | } 93 | const eventMap = node.arguments[0]; 94 | 95 | if (eventMap.type === 'ObjectExpression') { 96 | eventMap.properties.forEach(validateEventDefinition); 97 | } 98 | }, 99 | }; 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /lib/rules/no-dom-lookup-on-created.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Forbid DOM lookup in template creation callback 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const getPropertyName = require('../util/ast/getPropertyName'); 9 | 10 | const errorMessage = 11 | 'Accessing DOM from "onCreated" is forbidden. Try from "onRendered" instead.'; 12 | const jQueryNames = new Set(['$', 'jQuery']); 13 | 14 | const isJQueryCallee = (node) => 15 | // $() 16 | (node.type === 'Identifier' && jQueryNames.has(node.name)) || // Template.instance().$() 17 | (node.type === 'MemberExpression' && 18 | node.property.type === 'Identifier' && 19 | node.property.name === '$' && 20 | node.object.type === 'CallExpression' && 21 | node.object.callee.type === 'MemberExpression' && 22 | node.object.callee.object.type === 'Identifier' && 23 | node.object.callee.object.name === 'Template' && 24 | getPropertyName(node.object.callee.property) === 'instance'); 25 | 26 | const isRelevantTemplateCallExpression = (node) => 27 | node.type === 'CallExpression' && 28 | node.callee.type === 'MemberExpression' && 29 | node.callee.object.type === 'MemberExpression' && 30 | node.callee.object.object.type === 'Identifier' && 31 | node.callee.object.object.name === 'Template' && 32 | getPropertyName(node.callee.property) === 'onCreated'; 33 | 34 | const isInRelevantTemplateScope = (ancestors) => 35 | ancestors.some(isRelevantTemplateCallExpression); 36 | 37 | module.exports = { 38 | meta: { 39 | schema: [], 40 | }, 41 | create: (context) => ({ 42 | CallExpression: (node) => { 43 | if (!isJQueryCallee(node.callee)) return; 44 | if (!isInRelevantTemplateScope(context.getAncestors())) return; 45 | 46 | context.report(node, errorMessage); 47 | }, 48 | }), 49 | }; 50 | -------------------------------------------------------------------------------- /lib/rules/no-session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent usage of Session 3 | * @author Dominik Ferber 4 | */ 5 | 6 | // ----------------------------------------------------------------------------- 7 | // Rule Definition 8 | // ----------------------------------------------------------------------------- 9 | 10 | module.exports = { 11 | meta: { 12 | schema: [], 13 | }, 14 | create: (context) => ({ 15 | MemberExpression: (node) => { 16 | if (node.object.name === 'Session') { 17 | context.report(node, 'Unexpected Session statement'); 18 | } 19 | }, 20 | }), 21 | }; 22 | -------------------------------------------------------------------------------- /lib/rules/no-template-lifecycle-assignments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent deprecated template lifecycle callback assignments. 3 | * @author Dominik Ferber 4 | */ 5 | 6 | // ----------------------------------------------------------------------------- 7 | // Rule Definition 8 | // ----------------------------------------------------------------------------- 9 | 10 | module.exports = { 11 | meta: { 12 | schema: [], 13 | }, 14 | create: (context) => { 15 | // --------------------------------------------------------------------------- 16 | // Helpers 17 | // --------------------------------------------------------------------------- 18 | 19 | /* 20 | * Check if name is a forbidden property (rendered, created, destroyed) 21 | * @param {String} name The name of the property 22 | * @returns {Boolean} True if name is forbidden. 23 | */ 24 | function isForbidden(name) { 25 | return ['created', 'rendered', 'destroyed'].indexOf(name) !== -1; 26 | } 27 | 28 | function capitalizeFirstLetter(str) { 29 | return str.charAt(0).toUpperCase() + str.slice(1); 30 | } 31 | 32 | function reportError(node, propertyName) { 33 | context.report( 34 | node, 35 | // eslint-disable-next-line max-len 36 | `Template callback assignment with "${propertyName}" is deprecated. Use "on${capitalizeFirstLetter( 37 | propertyName 38 | )}" instead` 39 | ); 40 | } 41 | 42 | // --------------------------------------------------------------------------- 43 | // Public 44 | // --------------------------------------------------------------------------- 45 | return { 46 | AssignmentExpression: (node) => { 47 | if (node.operator === '=') { 48 | const lhs = node.left; 49 | if ( 50 | lhs.type === 'MemberExpression' && 51 | !lhs.computed && 52 | lhs.object.type === 'MemberExpression' && 53 | lhs.object.object.type === 'Identifier' && 54 | lhs.object.object.name === 'Template' && 55 | lhs.property.type === 'Identifier' && 56 | isForbidden(lhs.property.name) 57 | ) { 58 | reportError(node, lhs.property.name); 59 | } 60 | } 61 | }, 62 | }; 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /lib/rules/no-template-parent-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Avoid accessing template parent data 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | module.exports = { 9 | meta: { 10 | schema: [], 11 | }, 12 | create: (context) => ({ 13 | CallExpression: (node) => { 14 | if ( 15 | node.callee.type === 'MemberExpression' && 16 | node.callee.object.type === 'Identifier' && 17 | node.callee.object.name === 'Template' && 18 | node.callee.property.type === 'Identifier' && 19 | node.callee.property.name === 'parentData' 20 | ) { 21 | context.report(node, 'Forbidden. Pass data explicitly instead'); 22 | } 23 | }, 24 | }), 25 | }; 26 | -------------------------------------------------------------------------------- /lib/rules/no-zero-timeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent usage of Meteor.setTimeout with zero delay 3 | * @author Dominik Ferber 4 | */ 5 | 6 | const { isMeteorCall } = require('../util/ast'); 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Rule Definition 10 | // ----------------------------------------------------------------------------- 11 | 12 | module.exports = { 13 | meta: { 14 | schema: [], 15 | }, 16 | create: (context) => ({ 17 | CallExpression: (node) => { 18 | if (isMeteorCall(node, 'setTimeout')) { 19 | if (node.arguments.length === 1) { 20 | context.report(node, 'Implicit timeout of 0'); 21 | } else if ( 22 | node.arguments.length > 1 && 23 | node.arguments[1].type === 'Literal' && 24 | node.arguments[1].value === 0 25 | ) { 26 | context.report(node, 'Timeout of 0. Use `Meteor.defer` instead'); 27 | } 28 | } 29 | }, 30 | }), 31 | }; 32 | -------------------------------------------------------------------------------- /lib/rules/prefer-session-equals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prefer Session.equals in conditions 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const isSessionGetCallExpression = (node) => 9 | node.type === 'CallExpression' && 10 | node.callee.type === 'MemberExpression' && 11 | node.callee.object.type === 'Identifier' && 12 | node.callee.object.name === 'Session' && 13 | ((!node.callee.computed && 14 | node.callee.property.type === 'Identifier' && 15 | node.callee.property.name === 'get') || 16 | (node.callee.computed && 17 | node.callee.property.type === 'Literal' && 18 | node.callee.property.value === 'get')); 19 | 20 | // ----------------------------------------------------------------------------- 21 | // Rule Definition 22 | // ----------------------------------------------------------------------------- 23 | 24 | module.exports = { 25 | meta: { 26 | schema: [], 27 | }, 28 | create: (context) => { 29 | // --------------------------------------------------------------------------- 30 | // Helpers 31 | // --------------------------------------------------------------------------- 32 | const errorMessage = 'Use "Session.equals" instead'; 33 | 34 | const checkTest = (node) => { 35 | switch (node.type) { 36 | case 'BinaryExpression': 37 | case 'LogicalExpression': 38 | checkTest(node.left); 39 | checkTest(node.right); 40 | break; 41 | case 'CallExpression': 42 | if (isSessionGetCallExpression(node)) { 43 | context.report(node.callee, errorMessage); 44 | } 45 | break; 46 | default: 47 | break; 48 | } 49 | }; 50 | 51 | // --------------------------------------------------------------------------- 52 | // Public 53 | // --------------------------------------------------------------------------- 54 | return { 55 | ConditionalExpression: (node) => { 56 | checkTest(node.test); 57 | }, 58 | IfStatement: (node) => checkTest(node.test), 59 | }; 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /lib/rules/prefix-eventmap-selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Convention for eventmap selectors 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { isTemplateProp } = require('../util/ast'); 9 | 10 | // ----------------------------------------------------------------------------- 11 | // Rule Definition 12 | // ----------------------------------------------------------------------------- 13 | 14 | module.exports = { 15 | meta: { 16 | schema: [{ type: 'string' }, { enum: ['relaxed', 'strict'] }], 17 | }, 18 | create: (context) => { 19 | // --------------------------------------------------------------------------- 20 | // Helpers 21 | // --------------------------------------------------------------------------- 22 | 23 | const [prefix = 'js-', mode = 'relaxed'] = context.options; 24 | 25 | // algorithm to parse event map selector taken from blaze itself 26 | // https://github.com/meteor/meteor/blob/15a0369581ef27a6d3d49cb0110d10b1198d5383/packages/blaze/view.js#L867 27 | function validateEventDefinition(node) { 28 | if (node.key.type !== 'Literal') return; 29 | 30 | const spec = node.key.value; 31 | const clauses = spec.split(/,\s+/); 32 | clauses.forEach((clause) => { 33 | const parts = clause.split(/\s+/); 34 | 35 | if (parts.length === 1) { 36 | if (mode === 'strict') { 37 | context.report(node.key, 'Missing selector'); 38 | } 39 | return; 40 | } 41 | 42 | const selector = parts[1]; 43 | 44 | if (selector.startsWith('.')) { 45 | if (!selector.startsWith(`.${prefix}`)) { 46 | context.report( 47 | node.key, 48 | `Expected selector to be prefixed with "${prefix}"` 49 | ); 50 | } else if (selector === `.${prefix}`) { 51 | context.report(node.key, 'Selector may not consist of prefix only'); 52 | } 53 | } else if (mode === 'strict') { 54 | context.report(node.key, 'Expected selector to be a class'); 55 | } 56 | }); 57 | } 58 | 59 | // --------------------------------------------------------------------------- 60 | // Public 61 | // --------------------------------------------------------------------------- 62 | 63 | return { 64 | CallExpression: (node) => { 65 | if ( 66 | node.arguments.length === 0 || 67 | !isTemplateProp(node.callee, 'events') 68 | ) { 69 | return; 70 | } 71 | const eventMap = node.arguments[0]; 72 | 73 | if (eventMap.type === 'ObjectExpression') { 74 | eventMap.properties.forEach(validateEventDefinition); 75 | } 76 | }, 77 | }; 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /lib/rules/scope-dom-lookups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Scope DOM lookups to the template instance 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const getPropertyName = require('../util/ast/getPropertyName'); 9 | 10 | const jQueryNames = new Set(['$', 'jQuery']); 11 | 12 | const relevantTemplatePropertyNames = new Set([ 13 | 'onRendered', 14 | 'onDestroyed', 15 | 'events', 16 | 'helpers', 17 | ]); 18 | 19 | const isJQueryIdentifier = (node) => 20 | node.type === 'Identifier' && jQueryNames.has(node.name); 21 | 22 | const isRelevantTemplateCallExpression = (node) => 23 | node.type === 'CallExpression' && 24 | node.callee.type === 'MemberExpression' && 25 | node.callee.object.type === 'MemberExpression' && 26 | node.callee.object.object.type === 'Identifier' && 27 | node.callee.object.object.name === 'Template' && 28 | relevantTemplatePropertyNames.has(getPropertyName(node.callee.property)); 29 | 30 | const isInRelevantTemplateScope = (ancestors) => 31 | ancestors.some(isRelevantTemplateCallExpression); 32 | 33 | module.exports = { 34 | meta: { 35 | schema: [], 36 | }, 37 | create: (context) => ({ 38 | CallExpression: (node) => { 39 | if (!isJQueryIdentifier(node.callee)) return; 40 | if (!isInRelevantTemplateScope(context.getAncestors())) return; 41 | context.report(node, 'Use scoped DOM lookup instead'); 42 | }, 43 | }), 44 | }; 45 | -------------------------------------------------------------------------------- /lib/rules/template-names.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Force a naming convention for templates 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const values = require('../util/values'); 9 | 10 | const templateProps = new Set([ 11 | 'onCreated', 12 | 'onRendered', 13 | 'onDestroyed', 14 | 'events', 15 | 'helpers', 16 | 'created', 17 | 'rendered', 18 | 'destroyed', 19 | ]); 20 | 21 | const NAMING_CONVENTIONS = { 22 | CAMEL: 'camel-case', 23 | PASCAL: 'pascal-case', 24 | SNAKE: 'snake-case', 25 | UPPER_SNAKE: 'upper-snake-case', 26 | }; 27 | 28 | const isTemplateMemberExpression = (node) => 29 | node.object.type === 'MemberExpression' && 30 | node.object.object.type === 'Identifier' && 31 | node.object.object.name === 'Template' && 32 | (node.object.property.type === 'Identifier' || 33 | node.object.property.type === 'Literal') && 34 | node.property.type === 'Identifier' && 35 | templateProps.has(node.property.name); 36 | 37 | // assuming node type is always either Identifier or Literal 38 | const getNameOfProperty = (node) => 39 | node.type === 'Identifier' ? node.name : node.value; 40 | 41 | const getErrorMessage = (expected) => 42 | `Invalid template name, expected name to be in ${expected}`; 43 | 44 | module.exports = { 45 | meta: { 46 | schema: [{ enum: values(NAMING_CONVENTIONS) }], 47 | }, 48 | create: (context) => ({ 49 | MemberExpression: (node) => { 50 | if (!isTemplateMemberExpression(node)) return; 51 | 52 | const [namingConvention] = context.options; 53 | const templateName = getNameOfProperty(node.object.property); 54 | switch (namingConvention) { 55 | case NAMING_CONVENTIONS.PASCAL: 56 | if (!/^[A-Z]([A-Z]|[a-z]|[0-9])*$/.test(templateName)) { 57 | context.report(node, getErrorMessage(NAMING_CONVENTIONS.PASCAL)); 58 | } 59 | break; 60 | case NAMING_CONVENTIONS.SNAKE: 61 | if (!/^([a-z]|[0-9]|_)+$/i.test(templateName)) { 62 | context.report(node, getErrorMessage(NAMING_CONVENTIONS.SNAKE)); 63 | } 64 | break; 65 | case NAMING_CONVENTIONS.UPPER_SNAKE: 66 | if (!/^[A-Z]([a-z]|[A-Z]|[0-9]|_)+$/.test(templateName)) { 67 | context.report( 68 | node, 69 | getErrorMessage(NAMING_CONVENTIONS.UPPER_SNAKE) 70 | ); 71 | } 72 | break; 73 | case NAMING_CONVENTIONS.CAMEL: 74 | default: 75 | if (!/^[a-z]([A-Z]|[a-z]|[0-9])+$/.test(templateName)) { 76 | context.report(node, getErrorMessage(NAMING_CONVENTIONS.CAMEL)); 77 | } 78 | break; 79 | } 80 | }, 81 | }), 82 | }; 83 | -------------------------------------------------------------------------------- /lib/util/ast/getPropertyName.js: -------------------------------------------------------------------------------- 1 | module.exports = function getPropertyName(property) { 2 | if (property.type === 'Literal') { 3 | return property.value; 4 | } 5 | if (property.type === 'Identifier') { 6 | return property.name; 7 | } 8 | return false; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/util/ast/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isMeteorCall: require('./isMeteorCall'), 3 | isMeteorProp: require('./isMeteorProp'), 4 | isTemplateProp: require('./isTemplateProp'), 5 | isFunction: require('./isFunction'), 6 | getPropertyName: require('./getPropertyName'), 7 | }; 8 | -------------------------------------------------------------------------------- /lib/util/ast/isFunction.js: -------------------------------------------------------------------------------- 1 | module.exports = function isFunction(type) { 2 | return type === 'ArrowFunctionExpression' || type === 'FunctionExpression'; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/util/ast/isMeteorCall.js: -------------------------------------------------------------------------------- 1 | const isMeteorProp = require('./isMeteorProp'); 2 | 3 | module.exports = function isMeteorCall(node, propName) { 4 | return ( 5 | node.type === 'CallExpression' && 6 | node.callee.type === 'MemberExpression' && 7 | isMeteorProp(node.callee, propName) 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/util/ast/isMeteorProp.js: -------------------------------------------------------------------------------- 1 | module.exports = function isMeteorProp(node, propName) { 2 | return ( 3 | node.type === 'MemberExpression' && 4 | node.object.type === 'Identifier' && 5 | node.object.name === 'Meteor' && 6 | ((!node.computed && 7 | node.property.type === 'Identifier' && 8 | node.property.name === propName) || 9 | (node.computed && 10 | node.property.type === 'Literal' && 11 | node.property.value === propName)) 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/util/ast/isTemplateProp.js: -------------------------------------------------------------------------------- 1 | const getPropertyName = require('./getPropertyName'); 2 | 3 | module.exports = function isTemplateProp(node, propName) { 4 | return ( 5 | node.type === 'MemberExpression' && 6 | node.object.type === 'MemberExpression' && 7 | node.object.object.type === 'Identifier' && 8 | node.object.object.name === 'Template' && 9 | getPropertyName(node.property) === propName 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/util/environment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PUBLIC: '-public', 3 | PRIVATE: '-private', 4 | CLIENT: '-client', 5 | SERVER: '-server', 6 | PACKAGE: '-pkg', 7 | TEST: '-test', 8 | NODE_MODULE: '-node_module', 9 | UNIVERSAL: '-universal', 10 | PACKAGE_CONFIG: '-pkg-cfg', 11 | MOBILE_CONFIG: '-mobile-cfg', 12 | COMPATIBILITY: '-compat', 13 | NON_METEOR: '-non-meteor', 14 | }; 15 | -------------------------------------------------------------------------------- /lib/util/executors/filterExecutorsByAncestors.js: -------------------------------------------------------------------------------- 1 | const invariant = require('invariant'); 2 | const isMeteorBlockOnlyTest = require('./isMeteorBlockOnlyTest'); 3 | const getExecutorsFromTest = require('./getExecutorsFromTest'); 4 | const { intersection, difference } = require('./sets'); 5 | 6 | // Set -> Array -> Set 7 | module.exports = function filterExecutorsByAncestors( 8 | originalExecutors, 9 | ancestors 10 | ) { 11 | let executors = new Set([...originalExecutors]); 12 | 13 | for (let i = ancestors.length - 1; i > 0; i -= 1) { 14 | const current = ancestors[i]; 15 | const parent = ancestors[i - 1]; 16 | if (parent.type === 'IfStatement') { 17 | if (isMeteorBlockOnlyTest(parent.test)) { 18 | const executorsFromTest = getExecutorsFromTest(parent.test); 19 | if (parent.consequent === current) { 20 | executors = intersection(executors, executorsFromTest); 21 | } else if (parent.alternate === current) { 22 | executors = difference(executors, executorsFromTest); 23 | } else { 24 | invariant( 25 | false, 26 | 'Block is neither consequent nor alternate of parent' 27 | ); 28 | } 29 | } 30 | } 31 | } 32 | 33 | return executors; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/util/executors/getExecutors.js: -------------------------------------------------------------------------------- 1 | const filterExecutorsByAncestors = require('./filterExecutorsByAncestors'); 2 | const getExecutorsByEnv = require('./getExecutorsByEnv'); 3 | 4 | // ENVIRONMENT -> Context -> Set 5 | module.exports = function getExecutors(env, ancestors) { 6 | return filterExecutorsByAncestors(getExecutorsByEnv(env), ancestors); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/util/executors/getExecutorsByEnv.js: -------------------------------------------------------------------------------- 1 | const { 2 | PUBLIC, 3 | PRIVATE, 4 | CLIENT, 5 | SERVER, 6 | PACKAGE, 7 | TEST, 8 | NODE_MODULE, 9 | UNIVERSAL, 10 | PACKAGE_CONFIG, 11 | MOBILE_CONFIG, 12 | COMPATIBILITY, 13 | NON_METEOR, 14 | } = require('../environment'); 15 | 16 | /** 17 | * Transforms an environment into executors 18 | * @param {ENVIRONMENT} env An Environment 19 | * @return {Set} A Set of executors 20 | */ 21 | module.exports = function getExecutorsByEnv(env) { 22 | const executors = new Set(); 23 | switch (env) { 24 | case CLIENT: 25 | case COMPATIBILITY: 26 | executors.add('browser'); 27 | executors.add('cordova'); 28 | break; 29 | case SERVER: 30 | executors.add('server'); 31 | break; 32 | case UNIVERSAL: 33 | executors.add('server'); 34 | executors.add('browser'); 35 | executors.add('cordova'); 36 | break; 37 | case PACKAGE_CONFIG: 38 | case MOBILE_CONFIG: 39 | case PUBLIC: 40 | case PRIVATE: 41 | case TEST: 42 | case NODE_MODULE: 43 | case NON_METEOR: 44 | case PACKAGE: 45 | default: 46 | break; 47 | } 48 | return executors; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/util/executors/getExecutorsFromTest.js: -------------------------------------------------------------------------------- 1 | const invariant = require('invariant'); 2 | const isMeteorProp = require('../ast/isMeteorProp'); 3 | const { union, intersection } = require('./sets'); 4 | const invert = require('./invert'); 5 | 6 | // Nodes -> Set 7 | module.exports = function getExecutorsFromTest(test) { 8 | switch (test.type) { 9 | case 'MemberExpression': 10 | if (isMeteorProp(test, 'isClient')) { 11 | return new Set(['browser', 'cordova']); 12 | } 13 | if (isMeteorProp(test, 'isServer')) { 14 | return new Set(['server']); 15 | } 16 | if (isMeteorProp(test, 'isCordova')) { 17 | return new Set(['cordova']); 18 | } 19 | return invariant(false, 'Unkown Meteor prop should never be reached'); 20 | case 'UnaryExpression': 21 | return invert(getExecutorsFromTest(test.argument)); 22 | case 'LogicalExpression': 23 | if (test.operator === '&&') { 24 | return intersection( 25 | getExecutorsFromTest(test.left), 26 | getExecutorsFromTest(test.right) 27 | ); 28 | } 29 | if (test.operator === '||') { 30 | return union( 31 | getExecutorsFromTest(test.left), 32 | getExecutorsFromTest(test.right) 33 | ); 34 | } 35 | return invariant(false, 'Unkown operator should never be reached'); 36 | default: 37 | return invariant( 38 | false, 39 | 'Called getExecutorsFromTest on unkown node type' 40 | ); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/util/executors/invert.js: -------------------------------------------------------------------------------- 1 | const { difference } = require('./sets'); 2 | 3 | module.exports = function invert(executors) { 4 | return difference(new Set(['browser', 'server', 'cordova']), executors); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/util/executors/isMeteorBlockOnlyTest.js: -------------------------------------------------------------------------------- 1 | const { isMeteorProp } = require('../ast'); 2 | 3 | /** 4 | * Verifies a test of an IfStatement contains only checks with 5 | * Meteor.isClient, Meteor.isServer and Meteor.isCordova. 6 | * 7 | * @param {node} test Test of an IfStatement (MemberExpression, LogicalExpression, UnaryExpression) 8 | * @return {Boolean} True if test contains only Meteor locus checks 9 | */ 10 | module.exports = function isMeteorBlockOnlyTest(test) { 11 | switch (test.type) { 12 | case 'MemberExpression': 13 | return ( 14 | isMeteorProp(test, 'isClient') || 15 | isMeteorProp(test, 'isServer') || 16 | isMeteorProp(test, 'isCordova') 17 | ); 18 | case 'UnaryExpression': 19 | if (test.operator === '!') { 20 | return isMeteorBlockOnlyTest(test.argument); 21 | } 22 | return false; 23 | case 'LogicalExpression': 24 | return ( 25 | isMeteorBlockOnlyTest(test.left) && isMeteorBlockOnlyTest(test.right) 26 | ); 27 | default: 28 | return false; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/util/executors/sets.js: -------------------------------------------------------------------------------- 1 | const invariant = require('invariant'); 2 | 3 | // Set -> Set -> Set 4 | module.exports.difference = (a, b) => { 5 | invariant(!!a, 'difference: Set a is not defined'); 6 | invariant(!!b, 'difference: Set b is not defined'); 7 | return new Set([...a].filter((x) => !b.has(x))); 8 | }; 9 | 10 | // Set -> Set -> Set 11 | module.exports.union = (a, b) => { 12 | invariant(!!a, 'union: Set a is not defined'); 13 | invariant(!!b, 'union: Set b is not defined'); 14 | return new Set([...a, ...b]); 15 | }; 16 | 17 | // Set -> Set -> Set 18 | module.exports.intersection = (a, b) => { 19 | invariant(!!a, 'intersection: Set a is not defined'); 20 | invariant(!!b, 'intersection: Set b is not defined'); 21 | return new Set([...a].filter((element) => b.has(element))); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/util/values.js: -------------------------------------------------------------------------------- 1 | module.exports = (obj) => Object.keys(obj).map((key) => obj[key]); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-meteor", 3 | "version": "0.0.0-development", 4 | "author": "Dominik Ferber ", 5 | "description": "Meteor specific linting rules for ESLint", 6 | "main": "lib/index.js", 7 | "publishConfig": { 8 | "tag": "next" 9 | }, 10 | "release": { 11 | "branch": "master" 12 | }, 13 | "scripts": { 14 | "coverage:check": "nyc check-coverage --lines 100 --functions 100 --branches 100", 15 | "coverage:report": "nyc report", 16 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 17 | "format": "prettier --write 'lib/**/*.js' 'tests/**/*.js' 'scripts/**/*.js'", 18 | "lint": "eslint ./", 19 | "pretest": "npm run lint", 20 | "rule": "node scripts/new-rule.js", 21 | "test": "npm run unit-test && npm run coverage:report && npm run coverage:check", 22 | "unit-test": "nyc --reporter=lcov mocha tests --recursive", 23 | "unit-test:watch": "npm run unit-test -s -- --watch" 24 | }, 25 | "files": [ 26 | "LICENSE", 27 | "README.md", 28 | "lib" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/dferber90/eslint-plugin-meteor.git" 33 | }, 34 | "homepage": "https://github.com/dferber90/eslint-plugin-meteor", 35 | "bugs": "https://github.com/dferber90/eslint-plugin-meteor/issues", 36 | "dependencies": { 37 | "invariant": "2.2.4" 38 | }, 39 | "devDependencies": { 40 | "babel-eslint": "^10.1.0", 41 | "coveralls": "^3.1.0", 42 | "cz-conventional-changelog": "^3.3.0", 43 | "eslint": "^7.13.0", 44 | "eslint-config-prettier": "^6.15.0", 45 | "eslint-plugin-prettier": "^3.1.4", 46 | "mocha": "^8.2.1", 47 | "nyc": "^15.1.0", 48 | "prettier": "^2.1.2", 49 | "readline-sync": "^1.4.10", 50 | "semantic-release": "^17.2.2", 51 | "validate-commit-msg": "^2.14.0" 52 | }, 53 | "peerDependencies": { 54 | "eslint": ">= 3.7.0" 55 | }, 56 | "engines": { 57 | "node": ">=10" 58 | }, 59 | "keywords": [ 60 | "eslint", 61 | "eslint-plugin", 62 | "eslintplugin", 63 | "meteor" 64 | ], 65 | "config": { 66 | "ghooks": { 67 | "commit-msg": "node_modules/.bin/validate-commit-msg" 68 | }, 69 | "commitizen": { 70 | "path": "node_modules/cz-conventional-changelog/" 71 | } 72 | }, 73 | "license": "MIT", 74 | "contributors": [] 75 | } 76 | -------------------------------------------------------------------------------- /scripts/new-rule.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, max-len */ 2 | 3 | const readlineSync = require('readline-sync'); 4 | const colors = require('colors/safe'); 5 | const fs = require('fs'); 6 | 7 | console.log('Scaffolding new rule. Please give the following details.'); 8 | const authorName = readlineSync.question(colors.green('What is your name? ')); 9 | const ruleId = readlineSync.question(colors.green('What is the rule ID? ')); 10 | const desc = readlineSync.question( 11 | colors.green('Type a short description of this rule: ') 12 | ); 13 | const failingExample = readlineSync.question( 14 | colors.green('Type a short example of the code that will fail: ') 15 | ); 16 | const escapedFailingExample = failingExample.replace(/'/g, "\\'"); 17 | 18 | const doc = `# ${desc} (${ruleId}) 19 | 20 | Please describe the origin of the rule here. 21 | 22 | 23 | ## Rule Details 24 | 25 | This rule aims to... 26 | 27 | The following patterns are considered warnings: 28 | 29 | \`\`\`js 30 | 31 | // fill me in 32 | 33 | \`\`\` 34 | 35 | The following patterns are not warnings: 36 | 37 | \`\`\`js 38 | 39 | // fill me in 40 | 41 | \`\`\` 42 | 43 | ### Options 44 | 45 | If there are any options, describe them here. Otherwise, delete this section. 46 | 47 | ## When Not To Use It 48 | 49 | Give a short description of when it would be appropriate to turn off this rule. 50 | 51 | ## Further Reading 52 | 53 | If there are other links that describe the issue this rule addresses, please include them here in a bulleted list. 54 | 55 | `; 56 | 57 | const rule = `/** 58 | * @fileoverview ${desc} 59 | * @author ${authorName} 60 | * @copyright 2016 ${authorName}. All rights reserved. 61 | * See LICENSE file in root directory for full license. 62 | */ 63 | 64 | 65 | module.exports = { 66 | meta: { 67 | schema: [], 68 | }, 69 | create: (context) => { 70 | // --------------------------------------------------------------------------- 71 | // Helpers 72 | // --------------------------------------------------------------------------- 73 | 74 | // any helper functions should go here or else delete this section 75 | 76 | // --------------------------------------------------------------------------- 77 | // Public 78 | // --------------------------------------------------------------------------- 79 | 80 | return { 81 | // give me methods 82 | } 83 | } 84 | } 85 | 86 | 87 | `; 88 | 89 | const test = `/** 90 | * @fileoverview ${desc} 91 | * @author ${authorName} 92 | * @copyright 2016 ${authorName}. All rights reserved. 93 | * See LICENSE file in root directory for full license. 94 | */ 95 | 96 | const { RuleTester } = require('eslint') 97 | const rule = require('../../../lib/rules/${ruleId}') 98 | 99 | const ruleTester = new RuleTester() 100 | 101 | ruleTester.run('${ruleId}', rule, { 102 | valid: [ 103 | // give me some valid tests 104 | ], 105 | 106 | invalid: [ 107 | { 108 | code: '${escapedFailingExample}', 109 | errors: [ 110 | { message: 'The error message', type: 'MemberExpression' }, 111 | ], 112 | }, 113 | ], 114 | }) 115 | 116 | `; 117 | 118 | const docFileName = `docs/rules/${ruleId}.md`; 119 | const ruleFileName = `lib/rules/${ruleId}.js`; 120 | const testFileName = `tests/lib/rules/${ruleId}.js`; 121 | 122 | const writeOptions = { 123 | encoding: 'utf8', 124 | flag: 'wx', 125 | }; 126 | 127 | try { 128 | fs.writeFileSync(ruleFileName, rule, writeOptions); 129 | fs.writeFileSync(testFileName, test, writeOptions); 130 | fs.writeFileSync(docFileName, doc, writeOptions); 131 | 132 | console.log(''); 133 | console.log(colors.green('✓ ') + colors.white(`create ${ruleFileName}`)); 134 | console.log(colors.green('✓ ') + colors.white(`create ${testFileName}`)); 135 | console.log(colors.green('✓ ') + colors.white(`create ${docFileName}`)); 136 | } catch (e) { 137 | if (e.code === 'EEXIST') { 138 | console.log(colors.red(`Aborting because rule already exists (${e.path})`)); 139 | 140 | // clean up already created files 141 | switch (e.path) { 142 | case ruleFileName: 143 | break; 144 | case testFileName: 145 | fs.unlinkSync(ruleFileName); 146 | break; 147 | case docFileName: 148 | fs.unlinkSync(ruleFileName); 149 | fs.unlinkSync(testFileName); 150 | break; 151 | default: 152 | break; 153 | } 154 | } else { 155 | console.log(e); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { rules, configs } = require('../lib/index'); 5 | 6 | const ruleNames = fs 7 | .readdirSync(path.resolve(__dirname, '../lib/rules/')) 8 | .filter((f) => path.extname(f) === '.js') 9 | .map((f) => path.basename(f, '.js')); 10 | 11 | describe('all rule files should be exported by the plugin', () => { 12 | ruleNames.forEach((ruleName) => { 13 | it(`should export ${ruleName}`, () => { 14 | assert({}.hasOwnProperty.call(rules, ruleName)); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('configurations', () => { 20 | ruleNames.forEach((ruleName) => { 21 | it(`should have a recommended configuration for ${ruleName}`, () => { 22 | assert( 23 | {}.hasOwnProperty.call(configs.recommended.rules, `meteor/${ruleName}`) 24 | ); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/lib/rules/audit-argument-checks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce check on all arguments passed to methods and publish functions 3 | * @author Dominik Ferber 4 | */ 5 | 6 | // ----------------------------------------------------------------------------- 7 | // Requirements 8 | // ----------------------------------------------------------------------------- 9 | 10 | const { RuleTester } = require('eslint'); 11 | const rule = require('../../../lib/rules/audit-argument-checks'); 12 | 13 | // ----------------------------------------------------------------------------- 14 | // Tests 15 | // ----------------------------------------------------------------------------- 16 | 17 | const ruleTester = new RuleTester(); 18 | ruleTester.run('audit-argument-checks', rule, { 19 | valid: [ 20 | 'foo()', 21 | 22 | 'Meteor[x]()', 23 | 'Meteor["publish"]()', 24 | 'Meteor.publish()', 25 | 26 | { 27 | code: 'Meteor.publish("foo", function ({ x }) {})', 28 | parserOptions: { ecmaVersion: 6 }, 29 | }, 30 | { 31 | code: 'Meteor.publish("foo", () => {})', 32 | parserOptions: { ecmaVersion: 6 }, 33 | }, 34 | 'Meteor.publish("foo", function () {})', 35 | 'Meteor.publish("foo", function (bar) { check(bar, Match.Any); })', 36 | { 37 | code: 'Meteor.publish("foo", (bar) => { check(bar, Match.Any); })', 38 | parserOptions: { ecmaVersion: 6 }, 39 | }, 40 | 'Meteor.publish("foo", function (bar, baz) { check(bar, Match.Any); check(baz, Match.Any); })', 41 | { 42 | code: 'Meteor.publish("foo", function (bar) { checkId(bar); })', 43 | options: [{ checkEquivalents: ['checkId'] }], 44 | }, 45 | { 46 | code: 47 | 'Meteor.publish("foo", function (bar) { var r; r = checkId(bar); })', 48 | options: [{ checkEquivalents: ['checkId'] }], 49 | }, 50 | { 51 | code: 'Meteor.publish("foo", function () { checkId(); })', 52 | options: [{ checkEquivalents: ['checkId'] }], 53 | }, 54 | 55 | 'Meteor.methods()', 56 | 'Meteor.methods({ x: function () {} })', 57 | 'Meteor["methods"]({ x: function () {} })', 58 | 'Meteor.methods({ x: true })', 59 | { code: 'Meteor.methods({ x () {} })', parserOptions: { ecmaVersion: 6 } }, 60 | 'Meteor.methods({ x: function (bar) { check(bar, Match.Any); } })', 61 | { 62 | code: 'Meteor.methods({ x: function (bar) { checkId(bar); } })', 63 | options: [{ checkEquivalents: ['checkId'] }], 64 | }, 65 | 'Meteor.methods({ x: function (bar, baz) { check(bar, Match.Any); check(baz, Match.Any); } })', 66 | ` 67 | Meteor.methods({ 68 | sendEmail: function (to, from, subject, text) { 69 | check([to, from, subject, text], [String]); 70 | }, 71 | }) 72 | `, 73 | { 74 | code: ` 75 | Meteor.methods({ 76 | sendEmail (to, from, subject, text) { 77 | check([to, from, subject, text], [String]); 78 | }, 79 | }) 80 | `, 81 | parserOptions: { ecmaVersion: 6 }, 82 | }, 83 | { 84 | code: ` 85 | Meteor.methods({ 86 | barWellChecked (bar = null) { 87 | check(bar, Match.OneOf(Object, null)); 88 | } 89 | }) 90 | `, 91 | parserOptions: { ecmaVersion: 6 }, 92 | }, 93 | { 94 | code: ` 95 | Meteor.methods({ 96 | barWellChecked (foo, bar = null) { 97 | check(foo, String); 98 | check(bar, Match.OneOf(Object, null)); 99 | } 100 | }) 101 | `, 102 | parserOptions: { ecmaVersion: 6 }, 103 | }, 104 | ], 105 | 106 | invalid: [ 107 | { 108 | code: 'Meteor.publish("foo", function (bar) { foo(); })', 109 | errors: [ 110 | { 111 | message: '"bar" is not checked', 112 | type: 'Identifier', 113 | }, 114 | ], 115 | }, 116 | { 117 | code: 'Meteor["publish"]("foo", function (bar) { foo(); })', 118 | errors: [ 119 | { 120 | message: '"bar" is not checked', 121 | type: 'Identifier', 122 | }, 123 | ], 124 | }, 125 | { 126 | code: 'Meteor.publish("foo", function (bar) {})', 127 | errors: [ 128 | { 129 | message: '"bar" is not checked', 130 | type: 'Identifier', 131 | }, 132 | ], 133 | }, 134 | { 135 | code: 136 | 'Meteor.publish("foo", function (bar, baz) { check(bar, Match.Any); })', 137 | errors: [ 138 | { 139 | message: '"baz" is not checked', 140 | type: 'Identifier', 141 | }, 142 | ], 143 | }, 144 | { 145 | code: 'Meteor.methods({ foo: function (bar) {} })', 146 | errors: [ 147 | { 148 | message: '"bar" is not checked', 149 | type: 'Identifier', 150 | }, 151 | ], 152 | }, 153 | { 154 | code: 'Meteor.methods({ foo: function () {}, foo2: function (bar) {} })', 155 | errors: [ 156 | { 157 | message: '"bar" is not checked', 158 | type: 'Identifier', 159 | }, 160 | ], 161 | }, 162 | { 163 | code: 'Meteor.methods({ foo () {}, foo2 (bar) {} })', 164 | errors: [ 165 | { 166 | message: '"bar" is not checked', 167 | type: 'Identifier', 168 | }, 169 | ], 170 | parserOptions: { ecmaVersion: 6 }, 171 | }, 172 | { 173 | code: ` 174 | Meteor.methods({ 175 | foo () {}, 176 | foo2 (bar) { 177 | if (!Meteor.isServer) { 178 | check(bar, Meteor.any) 179 | } 180 | } 181 | }) 182 | `, 183 | errors: [ 184 | { 185 | message: '"bar" is not checked', 186 | type: 'Identifier', 187 | }, 188 | ], 189 | parserOptions: { ecmaVersion: 6 }, 190 | }, 191 | { 192 | code: ` 193 | Meteor.methods({ 194 | foo: (bar) => 2 195 | }) 196 | `, 197 | errors: [ 198 | { 199 | message: '"bar" is not checked', 200 | type: 'Identifier', 201 | }, 202 | ], 203 | parserOptions: { ecmaVersion: 6 }, 204 | }, 205 | { 206 | code: ` 207 | Meteor.methods({ 208 | sendEmail: function (to, from, subject, bar) { 209 | check([to, from, subject], [String]); 210 | } 211 | }) 212 | `, 213 | errors: [ 214 | { 215 | message: '"bar" is not checked', 216 | type: 'Identifier', 217 | }, 218 | ], 219 | }, 220 | { 221 | code: ` 222 | Meteor.methods({ 223 | sendEmail (to, from, subject, bar) { 224 | check([to, from, subject], [String]); 225 | } 226 | }) 227 | `, 228 | errors: [ 229 | { 230 | message: '"bar" is not checked', 231 | type: 'Identifier', 232 | }, 233 | ], 234 | parserOptions: { ecmaVersion: 6 }, 235 | }, 236 | { 237 | code: ` 238 | Meteor.methods({ 239 | sendEmail (to, from, subject, bar) { 240 | check([to, from, subject, 'bar'], [String]); 241 | } 242 | }) 243 | `, 244 | errors: [ 245 | { 246 | message: '"bar" is not checked', 247 | type: 'Identifier', 248 | }, 249 | ], 250 | parserOptions: { ecmaVersion: 6 }, 251 | }, 252 | { 253 | code: ` 254 | Meteor.methods({ 255 | barBadlyChecked (bar = null) { 256 | check(foo, Match.OneOf(Object, null)); 257 | } 258 | }) 259 | `, 260 | errors: [ 261 | { 262 | message: '"bar" is not checked', 263 | type: 'Identifier', 264 | }, 265 | ], 266 | parserOptions: { ecmaVersion: 6 }, 267 | }, 268 | ], 269 | }); 270 | -------------------------------------------------------------------------------- /tests/lib/rules/eventmap-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensures that the names of the arguments of event handlers are always the same 3 | * @author Philipp Sporrer, Dominik Ferber 4 | * @copyright 2016 Philipp Sporrer. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Requirements 10 | // ----------------------------------------------------------------------------- 11 | 12 | const { RuleTester } = require('eslint'); 13 | const rule = require('../../../lib/rules/eventmap-params'); 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | ruleTester.run('eventmap-params', rule, { 21 | valid: [ 22 | ` 23 | Foo.bar.events({ 24 | 'submit form': function (bar, baz) { 25 | // no error, because not on Template 26 | } 27 | }) 28 | `, 29 | ` 30 | Template.foo.events({ 31 | 'submit form': function (event) {} 32 | }) 33 | `, 34 | ` 35 | Template['foo'].events({ 36 | 'submit form': function (event) {} 37 | }) 38 | `, 39 | ` 40 | Template['foo']['events']({ 41 | 'submit form': function (event) {} 42 | }) 43 | `, 44 | ` 45 | Template.foo['events']({ 46 | 'submit form': function (event) {} 47 | }) 48 | `, 49 | ` 50 | Template.foo.events({ 51 | 'submit form': {} 52 | }) 53 | `, 54 | ` 55 | Template.foo.events() 56 | `, 57 | { 58 | code: ` 59 | const SharedFeature = Template.SharedFeatures; 60 | 61 | Template.SharedFeatures = { 62 | EVENT_HANDLERS: { 63 | 'click .some-classname': function (e, tmpl) { 64 | // 65 | }, 66 | }, 67 | } 68 | 69 | Template.foo.events({ ...SharedFeatures.EVENT_HANDLERS }) 70 | `, 71 | parserOptions: { 72 | ecmaFeatures: { 73 | globalReturn: false, 74 | impliedStrict: true, 75 | experimentalObjectRestSpread: true, 76 | jsx: true, 77 | generators: false, 78 | objectLiteralDuplicateProperties: false, 79 | }, 80 | ecmaVersion: 2018, 81 | sourceType: 'module', 82 | }, 83 | }, 84 | ` 85 | Template.foo.events(null) 86 | `, 87 | { 88 | code: ` 89 | Template.foo.events({ 90 | 'submit form': function (evt) {} 91 | }) 92 | `, 93 | options: [ 94 | { 95 | eventParamName: 'evt', 96 | }, 97 | ], 98 | }, 99 | { 100 | code: ` 101 | Template.foo.events({ 102 | 'submit form': function (evt, tmplInst) {} 103 | }) 104 | `, 105 | options: [ 106 | { 107 | eventParamName: 'evt', 108 | templateInstanceParamName: 'tmplInst', 109 | }, 110 | ], 111 | }, 112 | ` 113 | Template.foo.events({ 114 | 'submit form': function (event, templateInstance) {} 115 | }) 116 | `, 117 | { 118 | code: ` 119 | Template.foo.events({ 120 | 'submit form': (event, templateInstance) => {} 121 | }) 122 | `, 123 | parserOptions: { ecmaVersion: 6 }, 124 | }, 125 | ` 126 | Nontemplate.foo.events({ 127 | 'submit form': function (bar, baz) { 128 | // no error, because not on Template 129 | } 130 | }) 131 | `, 132 | ` 133 | if (Meteor.isCordova) { 134 | Template.foo.events({ 135 | 'submit form': function (event, templateInstance) {} 136 | }) 137 | } 138 | `, 139 | ` 140 | if (Meteor.isClient) { 141 | Template.foo.events({ 142 | 'submit form': function (event, templateInstance) {} 143 | }) 144 | } 145 | `, 146 | { 147 | code: ` 148 | if (Meteor.isClient) { 149 | Template.foo.events({ 150 | 'submit form': (event, templateInstance) => {} 151 | }) 152 | } 153 | `, 154 | parserOptions: { ecmaVersion: 6 }, 155 | }, 156 | { 157 | code: ` 158 | Template.foo.events({ 159 | 'submit form': function (evt, templateInstance) {} 160 | }) 161 | `, 162 | options: [ 163 | { 164 | eventParamName: 'evt', 165 | }, 166 | ], 167 | }, 168 | { 169 | code: ` 170 | Template.foo.events({ 171 | 'submit form': function (event, tmplInst) {} 172 | }) 173 | `, 174 | options: [ 175 | { 176 | templateInstanceParamName: 'tmplInst', 177 | }, 178 | ], 179 | }, 180 | { 181 | code: ` 182 | Template.foo.events({ 183 | 'submit form': ({ target: form }, { data }) => {} 184 | }) 185 | `, 186 | parserOptions: { ecmaVersion: 6 }, 187 | }, 188 | { 189 | code: ` 190 | Template.foo.events({ 191 | 'submit form': (event, { data }) => {} 192 | }) 193 | `, 194 | parserOptions: { ecmaVersion: 6 }, 195 | options: [ 196 | { 197 | preventDestructuring: 'event', 198 | }, 199 | ], 200 | }, 201 | { 202 | code: ` 203 | Template.foo.events({ 204 | 'submit form': (evt, { data }) => {} 205 | }) 206 | `, 207 | parserOptions: { ecmaVersion: 6 }, 208 | options: [ 209 | { 210 | preventDestructuring: 'event', 211 | eventParamName: 'evt', 212 | }, 213 | ], 214 | }, 215 | { 216 | code: ` 217 | Template.foo.events({ 218 | 'submit form': ({ target: form }, templateInstance) => {} 219 | }) 220 | `, 221 | parserOptions: { ecmaVersion: 6 }, 222 | options: [ 223 | { 224 | preventDestructuring: 'templateInstance', 225 | }, 226 | ], 227 | }, 228 | ], 229 | 230 | invalid: [ 231 | { 232 | code: ` 233 | Template.foo.events({ 234 | 'submit form': function (foo, bar) {} 235 | }) 236 | `, 237 | errors: [ 238 | { 239 | message: 'Invalid parameter name, use "event" instead', 240 | type: 'Identifier', 241 | }, 242 | { 243 | message: 'Invalid parameter name, use "templateInstance" instead', 244 | type: 'Identifier', 245 | }, 246 | ], 247 | }, 248 | { 249 | code: ` 250 | Template['foo'].events({ 251 | 'submit form': function (foo, bar) {} 252 | }) 253 | `, 254 | errors: [ 255 | { 256 | message: 'Invalid parameter name, use "event" instead', 257 | type: 'Identifier', 258 | }, 259 | { 260 | message: 'Invalid parameter name, use "templateInstance" instead', 261 | type: 'Identifier', 262 | }, 263 | ], 264 | }, 265 | { 266 | code: ` 267 | Template['foo']['events']({ 268 | 'submit form': function (foo, bar) {} 269 | }) 270 | `, 271 | errors: [ 272 | { 273 | message: 'Invalid parameter name, use "event" instead', 274 | type: 'Identifier', 275 | }, 276 | { 277 | message: 'Invalid parameter name, use "templateInstance" instead', 278 | type: 'Identifier', 279 | }, 280 | ], 281 | }, 282 | { 283 | code: ` 284 | Template.foo['events']({ 285 | 'submit form': function (foo, bar) {} 286 | }) 287 | `, 288 | errors: [ 289 | { 290 | message: 'Invalid parameter name, use "event" instead', 291 | type: 'Identifier', 292 | }, 293 | { 294 | message: 'Invalid parameter name, use "templateInstance" instead', 295 | type: 'Identifier', 296 | }, 297 | ], 298 | }, 299 | { 300 | code: ` 301 | Template.foo.events({ 302 | 'submit form': (foo, bar) => {} 303 | }) 304 | `, 305 | errors: [ 306 | { 307 | message: 'Invalid parameter name, use "event" instead', 308 | type: 'Identifier', 309 | }, 310 | { 311 | message: 'Invalid parameter name, use "templateInstance" instead', 312 | type: 'Identifier', 313 | }, 314 | ], 315 | parserOptions: { ecmaVersion: 6 }, 316 | }, 317 | { 318 | code: ` 319 | Template.foo.events({ 320 | 'submit form': function (foo, templateInstance) {} 321 | }) 322 | `, 323 | errors: [ 324 | { 325 | message: 'Invalid parameter name, use "event" instead', 326 | type: 'Identifier', 327 | }, 328 | ], 329 | }, 330 | { 331 | code: ` 332 | Template.foo.events({ 333 | 'submit form': function (event, bar) {} 334 | }) 335 | `, 336 | errors: [ 337 | { 338 | message: 'Invalid parameter name, use "templateInstance" instead', 339 | type: 'Identifier', 340 | }, 341 | ], 342 | }, 343 | { 344 | code: ` 345 | if (Meteor.isClient) { 346 | Template.foo.events({ 347 | 'submit form': function (foo, bar) {} 348 | }) 349 | } 350 | `, 351 | errors: [ 352 | { 353 | message: 'Invalid parameter name, use "event" instead', 354 | type: 'Identifier', 355 | }, 356 | { 357 | message: 'Invalid parameter name, use "templateInstance" instead', 358 | type: 'Identifier', 359 | }, 360 | ], 361 | }, 362 | { 363 | code: ` 364 | Template.foo.events({ 365 | 'submit form': function (foo, templateInstance) {} 366 | }) 367 | `, 368 | options: [ 369 | { 370 | eventParamName: 'evt', 371 | }, 372 | ], 373 | errors: [ 374 | { 375 | message: 'Invalid parameter name, use "evt" instead', 376 | type: 'Identifier', 377 | }, 378 | ], 379 | }, 380 | { 381 | code: ` 382 | Template.foo.events({ 383 | 'submit form': function (foo, instance) {} 384 | }) 385 | `, 386 | options: [ 387 | { 388 | templateInstanceParamName: 'instance', 389 | }, 390 | ], 391 | errors: [ 392 | { 393 | message: 'Invalid parameter name, use "event" instead', 394 | type: 'Identifier', 395 | }, 396 | ], 397 | }, 398 | { 399 | code: ` 400 | Template.foo.events({ 401 | 'submit form': function (evt, foo) {} 402 | }) 403 | `, 404 | options: [ 405 | { 406 | eventParamName: 'evt', 407 | }, 408 | ], 409 | errors: [ 410 | { 411 | message: 'Invalid parameter name, use "templateInstance" instead', 412 | type: 'Identifier', 413 | }, 414 | ], 415 | }, 416 | { 417 | code: ` 418 | Template.foo.events({ 419 | 'submit form': function (event, foo) {} 420 | }) 421 | `, 422 | options: [ 423 | { 424 | templateInstanceParamName: 'instance', 425 | }, 426 | ], 427 | errors: [ 428 | { 429 | message: 'Invalid parameter name, use "instance" instead', 430 | type: 'Identifier', 431 | }, 432 | ], 433 | }, 434 | { 435 | code: ` 436 | Template.foo.events({ 437 | 'submit form': ({ target: form }, templateInstance) => {} 438 | }) 439 | `, 440 | parserOptions: { ecmaVersion: 6 }, 441 | options: [ 442 | { 443 | preventDestructuring: 'event', 444 | }, 445 | ], 446 | errors: [ 447 | { 448 | message: 'Unexpected destructuring, use name "event"', 449 | type: 'ObjectPattern', 450 | }, 451 | ], 452 | }, 453 | { 454 | code: ` 455 | Template.foo.events({ 456 | 'submit form': (event, { data }) => {} 457 | }) 458 | `, 459 | parserOptions: { ecmaVersion: 6 }, 460 | options: [ 461 | { 462 | preventDestructuring: 'templateInstance', 463 | }, 464 | ], 465 | errors: [ 466 | { 467 | message: 'Unexpected destructuring, use name "templateInstance"', 468 | type: 'ObjectPattern', 469 | }, 470 | ], 471 | }, 472 | { 473 | code: ` 474 | Template.foo.events({ 475 | 'submit form': ({ target: form }, templateInstance) => {} 476 | }) 477 | `, 478 | parserOptions: { ecmaVersion: 6 }, 479 | options: [ 480 | { 481 | preventDestructuring: 'both', 482 | }, 483 | ], 484 | errors: [ 485 | { 486 | message: 'Unexpected destructuring, use name "event"', 487 | type: 'ObjectPattern', 488 | }, 489 | ], 490 | }, 491 | { 492 | code: ` 493 | Template.foo.events({ 494 | 'submit form': (event, { data }) => {} 495 | }) 496 | `, 497 | parserOptions: { ecmaVersion: 6 }, 498 | options: [ 499 | { 500 | preventDestructuring: 'both', 501 | templateInstanceParamName: 'instance', 502 | }, 503 | ], 504 | errors: [ 505 | { 506 | message: 'Unexpected destructuring, use name "instance"', 507 | type: 'ObjectPattern', 508 | }, 509 | ], 510 | }, 511 | ], 512 | }); 513 | -------------------------------------------------------------------------------- /tests/lib/rules/no-dom-lookup-on-created.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Forbid DOM lookup in template creation callback 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { RuleTester } = require('eslint'); 9 | const rule = require('../../../lib/rules/no-dom-lookup-on-created'); 10 | 11 | const ruleTester = new RuleTester(); 12 | 13 | ruleTester.run('no-dom-lookup-on-created', rule, { 14 | valid: [ 15 | '$(".bar").focus()', 16 | ` 17 | Template.foo.onRendered(function () { 18 | $('.bar').focus() 19 | }) 20 | `, 21 | ` 22 | Template.foo.onRendered(function () { 23 | this.$('.bar').focus() 24 | }) 25 | `, 26 | ` 27 | Template.foo.onRendered(function () { 28 | Template.instance().$('.bar').focus() 29 | }) 30 | `, 31 | ], 32 | 33 | invalid: [ 34 | { 35 | code: ` 36 | Template.foo.onCreated(function () { 37 | $('.bar').focus() 38 | }) 39 | `, 40 | errors: [ 41 | { 42 | message: 43 | 'Accessing DOM from "onCreated" is forbidden. Try from "onRendered" instead.', 44 | type: 'CallExpression', 45 | }, 46 | ], 47 | }, 48 | { 49 | code: ` 50 | Template.foo.onCreated(function () { 51 | Template.instance().$('.bar').focus() 52 | }) 53 | `, 54 | errors: [ 55 | { 56 | message: 57 | 'Accessing DOM from "onCreated" is forbidden. Try from "onRendered" instead.', 58 | type: 'CallExpression', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /tests/lib/rules/no-session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent usage of Session 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Requirements 10 | // ----------------------------------------------------------------------------- 11 | 12 | const { RuleTester } = require('eslint'); 13 | const rule = require('../../../lib/rules/no-session'); 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | ruleTester.run('no-session', rule, { 21 | valid: ['session.get("foo")', 'foo(Session)'], 22 | 23 | invalid: [ 24 | { 25 | code: ` 26 | if (Meteor.isCordova) { 27 | Session.set("foo", true) 28 | } 29 | `, 30 | errors: [ 31 | { message: 'Unexpected Session statement', type: 'MemberExpression' }, 32 | ], 33 | }, 34 | { 35 | code: 'Session.set("foo", true)', 36 | errors: [ 37 | { message: 'Unexpected Session statement', type: 'MemberExpression' }, 38 | ], 39 | }, 40 | { 41 | code: 'Session.get("foo")', 42 | errors: [ 43 | { message: 'Unexpected Session statement', type: 'MemberExpression' }, 44 | ], 45 | }, 46 | { 47 | code: 'Session.clear("foo")', 48 | errors: [ 49 | { message: 'Unexpected Session statement', type: 'MemberExpression' }, 50 | ], 51 | }, 52 | { 53 | code: 'Session.all()', 54 | errors: [ 55 | { message: 'Unexpected Session statement', type: 'MemberExpression' }, 56 | ], 57 | }, 58 | ], 59 | }); 60 | -------------------------------------------------------------------------------- /tests/lib/rules/no-template-lifecycle-assignments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent usage of deprecated template callback assignments. 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Requirements 10 | // ----------------------------------------------------------------------------- 11 | 12 | const { RuleTester } = require('eslint'); 13 | const rule = require('../../../lib/rules/no-template-lifecycle-assignments'); 14 | 15 | // ----------------------------------------------------------------------------- 16 | // Tests 17 | // ----------------------------------------------------------------------------- 18 | 19 | const ruleTester = new RuleTester(); 20 | 21 | ruleTester.run('no-template-lifecycle-assignments', rule, { 22 | valid: [ 23 | 'x += 1', 24 | 'Template = true', 25 | 'Template.foo.bar = true', 26 | 'Template.foo.onCreated(function () {})', 27 | 'Template.foo.onRendered(function () {})', 28 | 'Template.foo.onDestroyed(function () {})', 29 | ], 30 | 31 | invalid: [ 32 | { 33 | code: 'Template.foo.created = function () {}', 34 | errors: [ 35 | { 36 | message: 37 | 'Template callback assignment with "created" is deprecated. Use "onCreated" instead', 38 | type: 'AssignmentExpression', 39 | }, 40 | ], 41 | }, 42 | { 43 | code: ` 44 | if (Meteor.isCordova) { 45 | Template.foo.created = function () {} 46 | } 47 | `, 48 | errors: [ 49 | { 50 | message: 51 | 'Template callback assignment with "created" is deprecated. Use "onCreated" instead', 52 | type: 'AssignmentExpression', 53 | }, 54 | ], 55 | }, 56 | { 57 | code: 'Template.foo.rendered = function () {}', 58 | errors: [ 59 | { 60 | message: 61 | 'Template callback assignment with "rendered" is deprecated. Use "onRendered" instead', 62 | type: 'AssignmentExpression', 63 | }, 64 | ], 65 | }, 66 | { 67 | code: 'Template.foo.destroyed = function () {}', 68 | errors: [ 69 | { 70 | message: 71 | 'Template callback assignment with "destroyed" is deprecated. Use "onDestroyed" instead', 72 | type: 'AssignmentExpression', 73 | }, 74 | ], 75 | }, 76 | { 77 | code: 'Template["foo"].created = function () {}', 78 | errors: [ 79 | { 80 | message: 81 | 'Template callback assignment with "created" is deprecated. Use "onCreated" instead', 82 | type: 'AssignmentExpression', 83 | }, 84 | ], 85 | }, 86 | { 87 | code: 'Template["foo"].rendered = function () {}', 88 | errors: [ 89 | { 90 | message: 91 | 'Template callback assignment with "rendered" is deprecated. Use "onRendered" instead', 92 | type: 'AssignmentExpression', 93 | }, 94 | ], 95 | }, 96 | { 97 | code: 'Template["foo"].destroyed = function () {}', 98 | errors: [ 99 | { 100 | message: 101 | 'Template callback assignment with "destroyed" is deprecated. Use "onDestroyed" instead', 102 | type: 'AssignmentExpression', 103 | }, 104 | ], 105 | }, 106 | ], 107 | }); 108 | -------------------------------------------------------------------------------- /tests/lib/rules/no-template-parent-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Avoid accessing template parent data 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { RuleTester } = require('eslint'); 9 | const rule = require('../../../lib/rules/no-template-parent-data'); 10 | 11 | const ruleTester = new RuleTester(); 12 | 13 | ruleTester.run('no-template-parent-data', rule, { 14 | valid: ['Template.currentData()', 'Template.instance()', 'foo'], 15 | 16 | invalid: [ 17 | { 18 | code: 'Template.parentData()', 19 | errors: [ 20 | { 21 | message: 'Forbidden. Pass data explicitly instead', 22 | type: 'CallExpression', 23 | }, 24 | ], 25 | }, 26 | { 27 | code: 'Template.parentData(0)', 28 | errors: [ 29 | { 30 | message: 'Forbidden. Pass data explicitly instead', 31 | type: 'CallExpression', 32 | }, 33 | ], 34 | }, 35 | { 36 | code: 'Template.parentData(1)', 37 | errors: [ 38 | { 39 | message: 'Forbidden. Pass data explicitly instead', 40 | type: 'CallExpression', 41 | }, 42 | ], 43 | }, 44 | { 45 | code: 'Template.parentData(foo)', 46 | errors: [ 47 | { 48 | message: 'Forbidden. Pass data explicitly instead', 49 | type: 'CallExpression', 50 | }, 51 | ], 52 | }, 53 | ], 54 | }); 55 | -------------------------------------------------------------------------------- /tests/lib/rules/no-zero-timeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prevent usage of Meteor.setTimeout with zero delay 3 | * @author Dominik Ferber 4 | */ 5 | 6 | // ----------------------------------------------------------------------------- 7 | // Requirements 8 | // ----------------------------------------------------------------------------- 9 | 10 | const { RuleTester } = require('eslint'); 11 | const rule = require('../../../lib/rules/no-zero-timeout'); 12 | 13 | // ----------------------------------------------------------------------------- 14 | // Tests 15 | // ----------------------------------------------------------------------------- 16 | 17 | const ruleTester = new RuleTester(); 18 | ruleTester.run('no-zero-timeout', rule, { 19 | valid: [ 20 | 'Meteor.setTimeout()', 21 | 'Meteor.setTimeout(function () {}, 1)', 22 | 'Meteor.setTimeout(foo, 1)', 23 | 'Meteor.defer(foo, 0)', 24 | 'Meteor["setTimeout"](function () {}, 1)', 25 | 'Meteor["setInterval"](function () {}, 1)', 26 | 'foo()', 27 | ], 28 | 29 | invalid: [ 30 | { 31 | code: 'Meteor.setTimeout(function () {}, 0)', 32 | errors: [ 33 | { 34 | message: 'Timeout of 0. Use `Meteor.defer` instead', 35 | type: 'CallExpression', 36 | }, 37 | ], 38 | }, 39 | { 40 | code: 'Meteor["setTimeout"](function () {}, 0)', 41 | errors: [ 42 | { 43 | message: 'Timeout of 0. Use `Meteor.defer` instead', 44 | type: 'CallExpression', 45 | }, 46 | ], 47 | }, 48 | { 49 | code: 'Meteor.setTimeout(foo, 0)', 50 | errors: [ 51 | { 52 | message: 'Timeout of 0. Use `Meteor.defer` instead', 53 | type: 'CallExpression', 54 | }, 55 | ], 56 | }, 57 | { 58 | code: 'Meteor.setTimeout(function () {})', 59 | errors: [ 60 | { 61 | message: 'Implicit timeout of 0', 62 | type: 'CallExpression', 63 | }, 64 | ], 65 | }, 66 | { 67 | code: 'Meteor.setTimeout(foo)', 68 | errors: [ 69 | { 70 | message: 'Implicit timeout of 0', 71 | type: 'CallExpression', 72 | }, 73 | ], 74 | }, 75 | ], 76 | }); 77 | -------------------------------------------------------------------------------- /tests/lib/rules/prefer-session-equals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Prefer Session.equals in conditions 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Requirements 10 | // ----------------------------------------------------------------------------- 11 | 12 | const { RuleTester } = require('eslint'); 13 | const rule = require('../../../lib/rules/prefer-session-equals'); 14 | 15 | const ruleTester = new RuleTester(); 16 | 17 | ruleTester.run('prefer-session-equals', rule, { 18 | valid: [ 19 | 'var x = 1', 20 | 'if(x()) {}', 21 | 'if (true) {}', 22 | 'if (Session["equals"]("foo", true)) {}', 23 | 'if (Session.equals("foo", true)) {}', 24 | 'if (Session.equals("foo", false)) {}', 25 | 'if (Session.equals("foo", 1)) {}', 26 | 'if (Session.equals("foo", "hello")) {}', 27 | 'if (!Session.equals("foo", "hello")) {}', 28 | 'if (_.isEqual(Session.get("foo"), otherValue)) {}', 29 | 'Session.equals("foo", true) ? true : false', 30 | 'if (Session.set("foo")) {}', 31 | ], 32 | 33 | invalid: [ 34 | { 35 | code: 'if (Session.get("foo")) {}', 36 | errors: [ 37 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 38 | ], 39 | }, 40 | { 41 | code: 'if (Session.get("foo") == 3) {}', 42 | errors: [ 43 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 44 | ], 45 | }, 46 | { 47 | code: 'if (Session.get("foo") === 3) {}', 48 | errors: [ 49 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 50 | ], 51 | }, 52 | { 53 | code: 'if (Session.get("foo") === bar) {}', 54 | errors: [ 55 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 56 | ], 57 | }, 58 | { 59 | code: 'if (Session.get("foo") !== bar) {}', 60 | errors: [ 61 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 62 | ], 63 | }, 64 | { 65 | code: 'Session.get("foo") ? true : false', 66 | errors: [ 67 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 68 | ], 69 | }, 70 | { 71 | code: 'Session.get("foo") && false ? true : false', 72 | errors: [ 73 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 74 | ], 75 | }, 76 | { 77 | code: 'Session.get("foo") === 2 ? true : false', 78 | errors: [ 79 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 80 | ], 81 | }, 82 | { 83 | code: 'true || Session.get("foo") === 2 ? true : false', 84 | errors: [ 85 | { message: 'Use "Session.equals" instead', type: 'MemberExpression' }, 86 | ], 87 | }, 88 | ], 89 | }); 90 | -------------------------------------------------------------------------------- /tests/lib/rules/prefix-eventmap-selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Convention for eventmap selectors 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { RuleTester } = require('eslint'); 9 | const rule = require('../../../lib/rules/prefix-eventmap-selectors'); 10 | 11 | const ruleTester = new RuleTester(); 12 | 13 | ruleTester.run('prefix-eventmap-selectors', rule, { 14 | valid: [ 15 | // ------------------------------------------------------------------------ 16 | // Relaxed mode (default) 17 | // ------------------------------------------------------------------------ 18 | 'Template.foo.events({"click .js-foo": function () {}})', 19 | { 20 | code: 'Template.foo.events({"click .js-foo"() {}})', 21 | parserOptions: { ecmaVersion: 6 }, 22 | }, 23 | 'Template.foo.events({"blur .js-bar": function () {}})', 24 | 'Template.foo.events({"click": function () {}})', 25 | 'Template.foo.events({"click": function () {}, "click .js-bar": function () {}})', 26 | ` 27 | Template.foo.events({ 28 | 'click .js-foo': function () {}, 29 | 'blur .js-bar': function () {}, 30 | 'click #foo': function () {}, 31 | 'click [data-foo="bar"]': function () {}, 32 | 'click input': function () {}, 33 | 'click': function () {}, 34 | }) 35 | `, 36 | // ------------------------------------------------------------------------ 37 | // Strict mode 38 | // ------------------------------------------------------------------------ 39 | { 40 | code: 'Template.foo.events({"click .js-foo": function () {}})', 41 | options: ['js-', 'strict'], 42 | }, 43 | // ------------------------------------------------------------------------ 44 | // Prefix 45 | // ------------------------------------------------------------------------ 46 | { 47 | code: 'Template.foo.events({"click .bar-foo": function () {}})', 48 | options: ['bar-'], 49 | }, 50 | // ------------------------------------------------------------------------ 51 | // Edge cases 52 | // ------------------------------------------------------------------------ 53 | { 54 | code: 'Template.foo.events({[bar]: function () {}})', 55 | parserOptions: { ecmaVersion: 6 }, 56 | }, 57 | 'Template.foo.events(foo)', 58 | 'Template.foo.events()', 59 | 'Template.foo.helpers()', 60 | ], 61 | 62 | invalid: [ 63 | // ------------------------------------------------------------------------ 64 | // Relaxed mode (default) 65 | // ------------------------------------------------------------------------ 66 | { 67 | code: 'Template.foo.events({"click .foo": function () {}})', 68 | errors: [ 69 | { 70 | message: 'Expected selector to be prefixed with "js-"', 71 | type: 'Literal', 72 | }, 73 | ], 74 | }, 75 | { 76 | code: 'Template.foo.events({"click .foo"() {}})', 77 | parserOptions: { ecmaVersion: 6 }, 78 | errors: [ 79 | { 80 | message: 'Expected selector to be prefixed with "js-"', 81 | type: 'Literal', 82 | }, 83 | ], 84 | }, 85 | { 86 | code: 'Template.foo.events({"click .foo": () => {}})', 87 | parserOptions: { ecmaVersion: 6 }, 88 | errors: [ 89 | { 90 | message: 'Expected selector to be prefixed with "js-"', 91 | type: 'Literal', 92 | }, 93 | ], 94 | }, 95 | { 96 | code: 97 | 'Template.foo.events({"click .js-foo": () => {}, "click .foo": () => {}})', 98 | parserOptions: { ecmaVersion: 6 }, 99 | errors: [ 100 | { 101 | message: 'Expected selector to be prefixed with "js-"', 102 | type: 'Literal', 103 | }, 104 | ], 105 | }, 106 | // ------------------------------------------------------------------------ 107 | // Strict mode 108 | // ------------------------------------------------------------------------ 109 | { 110 | code: 111 | 'Template.foo.events({"click .js-foo": () => {}, "click input": () => {}})', 112 | options: ['js-', 'strict'], 113 | parserOptions: { ecmaVersion: 6 }, 114 | errors: [{ message: 'Expected selector to be a class', type: 'Literal' }], 115 | }, 116 | { 117 | code: 'Template.foo.events({"click": () => {}})', 118 | options: ['js-', 'strict'], 119 | parserOptions: { ecmaVersion: 6 }, 120 | errors: [{ message: 'Missing selector', type: 'Literal' }], 121 | }, 122 | { 123 | code: 'Template.foo.events({"click #js-xy": function () {}})', 124 | options: ['js-', 'strict'], 125 | errors: [{ message: 'Expected selector to be a class', type: 'Literal' }], 126 | }, 127 | { 128 | code: 'Template.foo.events({"click [data-foo=bar]": function () {}})', 129 | options: ['js-', 'strict'], 130 | errors: [{ message: 'Expected selector to be a class', type: 'Literal' }], 131 | }, 132 | { 133 | code: ` 134 | Template.foo.events({ 135 | "click": () => {}, 136 | "click .foo": () => {}, 137 | "click input": () => {}, 138 | "click .js-foo, blur input": () => {}, 139 | }) 140 | `, 141 | options: ['js-', 'strict'], 142 | parserOptions: { ecmaVersion: 6 }, 143 | errors: [ 144 | { message: 'Missing selector', type: 'Literal' }, 145 | { 146 | message: 'Expected selector to be prefixed with "js-"', 147 | type: 'Literal', 148 | }, 149 | { message: 'Expected selector to be a class', type: 'Literal' }, 150 | { message: 'Expected selector to be a class', type: 'Literal' }, 151 | ], 152 | }, 153 | // ------------------------------------------------------------------------ 154 | // Prefix 155 | // ------------------------------------------------------------------------ 156 | { 157 | code: 'Template.foo.events({"click .js-foo": () => {}})', 158 | options: ['bar-'], 159 | parserOptions: { ecmaVersion: 6 }, 160 | errors: [ 161 | { 162 | message: 'Expected selector to be prefixed with "bar-"', 163 | type: 'Literal', 164 | }, 165 | ], 166 | }, 167 | // ------------------------------------------------------------------------ 168 | // Edge cases 169 | // ------------------------------------------------------------------------ 170 | { 171 | code: 'Template.foo.events({"click .js-": function () {}})', 172 | errors: [ 173 | { message: 'Selector may not consist of prefix only', type: 'Literal' }, 174 | ], 175 | }, 176 | ], 177 | }); 178 | -------------------------------------------------------------------------------- /tests/lib/rules/scope-dom-lookups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Scope DOM lookups to the template instance 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | const { RuleTester } = require('eslint'); 9 | const rule = require('../../../lib/rules/scope-dom-lookups'); 10 | 11 | const ruleTester = new RuleTester(); 12 | 13 | ruleTester.run('scope-dom-lookups', rule, { 14 | valid: [ 15 | '$(".foo")', 16 | 'Template.foo.xyz(function () { $(".foo"); })', 17 | ` 18 | Template.foo.onRendered(function () { 19 | this.$('.bar').addClass('baz') 20 | }) 21 | `, 22 | ` 23 | Template.foo.onRendered(function () { 24 | Template.instance().$('.bar').addClass('.baz') 25 | }) 26 | `, 27 | ` 28 | Template.foo.events({ 29 | 'click .js-bar': function (event, instance) { 30 | instance.$('.baz').focus() 31 | } 32 | }) 33 | `, 34 | ], 35 | 36 | invalid: [ 37 | { 38 | code: ` 39 | Template.foo.onRendered(function () { 40 | $('.bar').addClass('baz') 41 | }) 42 | `, 43 | errors: [ 44 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 45 | ], 46 | }, 47 | { 48 | code: ` 49 | Template.foo.events({ 50 | 'click .js-bar': function (event, instance) { 51 | $('.baz').focus() 52 | } 53 | }) 54 | `, 55 | errors: [ 56 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 57 | ], 58 | }, 59 | { 60 | code: ` 61 | Template.foo.onRendered(function () { 62 | var $bar = $('.bar') 63 | $bar.addClass('baz') 64 | }) 65 | `, 66 | errors: [ 67 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 68 | ], 69 | }, 70 | { 71 | code: ` 72 | Template.foo.helpers({ 73 | 'bar': function () { 74 | $('.baz').focus() 75 | } 76 | }) 77 | `, 78 | errors: [ 79 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 80 | ], 81 | }, 82 | { 83 | code: ` 84 | Template.foo.onDestroyed(function () { 85 | $('.bar').addClass('baz') 86 | }) 87 | `, 88 | errors: [ 89 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 90 | ], 91 | }, 92 | { 93 | code: ` 94 | Template.foo.onRendered(function () { 95 | jQuery('.bar').addClass('baz') 96 | }) 97 | `, 98 | errors: [ 99 | { message: 'Use scoped DOM lookup instead', type: 'CallExpression' }, 100 | ], 101 | }, 102 | ], 103 | }); 104 | -------------------------------------------------------------------------------- /tests/lib/rules/template-names.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Force a naming convention for templates 3 | * @author Dominik Ferber 4 | * @copyright 2016 Dominik Ferber. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | // ----------------------------------------------------------------------------- 9 | // Requirements 10 | // ----------------------------------------------------------------------------- 11 | 12 | const { RuleTester } = require('eslint'); 13 | const rule = require('../../../lib/rules/template-names'); 14 | 15 | const ruleTester = new RuleTester(); 16 | 17 | ruleTester.run('template-names', rule, { 18 | valid: [ 19 | 'Template["foo"].helpers', 20 | 'Template.foo.helpers', 21 | 'Template.foo01.helpers', 22 | 'Template.foo19bar.helpers', 23 | 'Template.fooBar.helpers', 24 | 'Template.fooBar.helpers({})', 25 | { 26 | code: 'Template.FooBar.helpers({})', 27 | options: ['pascal-case'], 28 | }, 29 | { 30 | code: 'Template.foo_bar.helpers({})', 31 | options: ['snake-case'], 32 | }, 33 | { 34 | code: 'Template.Foo_bar.helpers({})', 35 | options: ['upper-snake-case'], 36 | }, 37 | { 38 | code: 'Template.fooBar.helpers({})', 39 | options: ['camel-case'], 40 | }, 41 | { 42 | code: 'Template.fooBar.helpers({})', 43 | options: [], 44 | }, 45 | ], 46 | 47 | invalid: [ 48 | { 49 | code: 'Template.foo_bar.onCreated', 50 | errors: [ 51 | { 52 | message: 'Invalid template name, expected name to be in camel-case', 53 | type: 'MemberExpression', 54 | }, 55 | ], 56 | }, 57 | { 58 | code: 'Template.foo_bar.onRendered', 59 | errors: [ 60 | { 61 | message: 'Invalid template name, expected name to be in camel-case', 62 | type: 'MemberExpression', 63 | }, 64 | ], 65 | }, 66 | { 67 | code: 'Template.foo_bar.onDestroyed', 68 | errors: [ 69 | { 70 | message: 'Invalid template name, expected name to be in camel-case', 71 | type: 'MemberExpression', 72 | }, 73 | ], 74 | }, 75 | { 76 | code: 'Template.foo_bar.events', 77 | errors: [ 78 | { 79 | message: 'Invalid template name, expected name to be in camel-case', 80 | type: 'MemberExpression', 81 | }, 82 | ], 83 | }, 84 | { 85 | code: 'Template.foo_bar.helpers', 86 | errors: [ 87 | { 88 | message: 'Invalid template name, expected name to be in camel-case', 89 | type: 'MemberExpression', 90 | }, 91 | ], 92 | }, 93 | { 94 | code: 'Template.foo_bar.created', 95 | errors: [ 96 | { 97 | message: 'Invalid template name, expected name to be in camel-case', 98 | type: 'MemberExpression', 99 | }, 100 | ], 101 | }, 102 | { 103 | code: 'Template.foo_bar.rendered', 104 | errors: [ 105 | { 106 | message: 'Invalid template name, expected name to be in camel-case', 107 | type: 'MemberExpression', 108 | }, 109 | ], 110 | }, 111 | { 112 | code: 'Template.foo_bar.destroyed', 113 | errors: [ 114 | { 115 | message: 'Invalid template name, expected name to be in camel-case', 116 | type: 'MemberExpression', 117 | }, 118 | ], 119 | }, 120 | { 121 | code: 'Template.foo_bar.helpers({})', 122 | errors: [ 123 | { 124 | message: 'Invalid template name, expected name to be in camel-case', 125 | type: 'MemberExpression', 126 | }, 127 | ], 128 | }, 129 | { 130 | code: 'Template.foo_bar.helpers({})', 131 | options: ['pascal-case'], 132 | errors: [ 133 | { 134 | message: 'Invalid template name, expected name to be in pascal-case', 135 | type: 'MemberExpression', 136 | }, 137 | ], 138 | }, 139 | { 140 | code: 'Template["foo-bar"].helpers({})', 141 | options: ['snake-case'], 142 | errors: [ 143 | { 144 | message: 'Invalid template name, expected name to be in snake-case', 145 | type: 'MemberExpression', 146 | }, 147 | ], 148 | }, 149 | { 150 | code: 'Template["foo_bar"].helpers({})', 151 | options: ['upper-snake-case'], 152 | errors: [ 153 | { 154 | message: 155 | 'Invalid template name, expected name to be in upper-snake-case', 156 | type: 'MemberExpression', 157 | }, 158 | ], 159 | }, 160 | ], 161 | }); 162 | -------------------------------------------------------------------------------- /tests/lib/util/ast/getPropertyName.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const getPropertyName = require('../../../../lib/util/ast/getPropertyName'); 3 | 4 | describe('getPropertyName', () => { 5 | it('returns false if property type is not Literal or Identifier', () => { 6 | assert.equal(getPropertyName({ type: 'CallExpression' }), false); 7 | }); 8 | 9 | it('returns the value if property type is of type Literal', () => { 10 | assert.equal(getPropertyName({ type: 'Literal', value: 'foo' }), 'foo'); 11 | }); 12 | 13 | it('returns the name if property type is of type Identifier', () => { 14 | assert.equal(getPropertyName({ type: 'Identifier', name: 'foo' }), 'foo'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/lib/util/ast/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const astUtils = require('../../../../lib/util/ast/index'); 3 | 4 | describe('ast utils', () => { 5 | it('exports isMeteorCall', () => { 6 | assert({}.hasOwnProperty.call(astUtils, 'isMeteorCall')); 7 | assert.equal(typeof astUtils.isMeteorCall, 'function'); 8 | }); 9 | it('exports isMeteorProp', () => { 10 | assert({}.hasOwnProperty.call(astUtils, 'isMeteorProp')); 11 | assert.equal(typeof astUtils.isMeteorProp, 'function'); 12 | }); 13 | it('exports isTemplateProp', () => { 14 | assert({}.hasOwnProperty.call(astUtils, 'isTemplateProp')); 15 | assert.equal(typeof astUtils.isTemplateProp, 'function'); 16 | }); 17 | it('exports isFunction', () => { 18 | assert({}.hasOwnProperty.call(astUtils, 'isFunction')); 19 | assert.equal(typeof astUtils.isFunction, 'function'); 20 | }); 21 | it('exports getPropertyName', () => { 22 | assert({}.hasOwnProperty.call(astUtils, 'getPropertyName')); 23 | assert.equal(typeof astUtils.getPropertyName, 'function'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/lib/util/ast/isMeteorCall.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const isMeteorCall = require('../../../../lib/util/ast/isMeteorCall'); 3 | 4 | describe('isMeteorCall', () => { 5 | it('returns true if node is a Meteor call', () => { 6 | assert.equal( 7 | isMeteorCall( 8 | { 9 | type: 'CallExpression', 10 | callee: { 11 | type: 'MemberExpression', 12 | computed: false, 13 | object: { 14 | type: 'Identifier', 15 | name: 'Meteor', 16 | }, 17 | property: { 18 | type: 'Identifier', 19 | name: 'foo', 20 | }, 21 | }, 22 | }, 23 | 'foo' 24 | ), 25 | true 26 | ); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/lib/util/executors/filterExecutorsByAncestors.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const filterExecutorsByAncestors = require('../../../../lib/util/executors/filterExecutorsByAncestors'); 3 | 4 | describe('filterExecutorsByAncestors', () => { 5 | it('filters on MemberExpression for isClient', () => { 6 | const consequent = { type: 'BlockStatement' }; 7 | const result = filterExecutorsByAncestors(new Set(['browser', 'server']), [ 8 | { type: 'Program' }, 9 | { 10 | type: 'IfStatement', 11 | test: { 12 | type: 'MemberExpression', 13 | object: { 14 | type: 'Identifier', 15 | name: 'Meteor', 16 | }, 17 | property: { 18 | type: 'Identifier', 19 | name: 'isClient', 20 | }, 21 | }, 22 | consequent, 23 | }, 24 | consequent, 25 | ]); 26 | assert.equal(result.size, 1); 27 | assert.ok(result.has('browser')); 28 | }); 29 | 30 | it('filters on MemberExpression for else-block of isClient', () => { 31 | const alternate = { type: 'BlockStatement' }; 32 | const result = filterExecutorsByAncestors(new Set(['browser', 'server']), [ 33 | { type: 'Program' }, 34 | { 35 | type: 'IfStatement', 36 | test: { 37 | type: 'MemberExpression', 38 | object: { 39 | type: 'Identifier', 40 | name: 'Meteor', 41 | }, 42 | property: { 43 | type: 'Identifier', 44 | name: 'isClient', 45 | }, 46 | }, 47 | alternate, 48 | }, 49 | alternate, 50 | ]); 51 | assert.equal(result.size, 1); 52 | assert.ok(result.has('server')); 53 | }); 54 | 55 | it('warns on hierarchical error', () => { 56 | assert.throws(() => { 57 | const consequent = { type: 'BlockStatement' }; 58 | filterExecutorsByAncestors(new Set(['browser', 'server']), [ 59 | { type: 'Program' }, 60 | { 61 | type: 'IfStatement', 62 | test: { 63 | type: 'MemberExpression', 64 | object: { 65 | type: 'Identifier', 66 | name: 'Meteor', 67 | }, 68 | property: { 69 | type: 'Identifier', 70 | name: 'isClient', 71 | }, 72 | }, 73 | }, 74 | consequent, 75 | ]); 76 | }); 77 | }); 78 | 79 | it('filters on MemberExpression for isServer', () => { 80 | const consequent = { type: 'BlockStatement' }; 81 | const result = filterExecutorsByAncestors(new Set(['server', 'cordova']), [ 82 | { type: 'Program' }, 83 | { 84 | type: 'IfStatement', 85 | test: { 86 | type: 'MemberExpression', 87 | object: { 88 | type: 'Identifier', 89 | name: 'Meteor', 90 | }, 91 | property: { 92 | type: 'Identifier', 93 | name: 'isServer', 94 | }, 95 | }, 96 | consequent, 97 | }, 98 | consequent, 99 | ]); 100 | assert.equal(result.size, 1); 101 | assert.ok(result.has('server')); 102 | }); 103 | 104 | it('filters on MemberExpression for isCordova', () => { 105 | const consequent = { type: 'BlockStatement' }; 106 | const result = filterExecutorsByAncestors(new Set(['browser', 'cordova']), [ 107 | { type: 'Program' }, 108 | { 109 | type: 'IfStatement', 110 | test: { 111 | type: 'MemberExpression', 112 | object: { 113 | type: 'Identifier', 114 | name: 'Meteor', 115 | }, 116 | property: { 117 | type: 'Identifier', 118 | name: 'isCordova', 119 | }, 120 | }, 121 | consequent, 122 | }, 123 | consequent, 124 | ]); 125 | assert.equal(result.size, 1); 126 | assert.ok(result.has('cordova')); 127 | }); 128 | 129 | it('filters on UnaryExpression', () => { 130 | const consequent = { type: 'BlockStatement' }; 131 | const result = filterExecutorsByAncestors( 132 | new Set(['browser', 'server', 'cordova']), 133 | [ 134 | { type: 'Program' }, 135 | { 136 | type: 'IfStatement', 137 | test: { 138 | type: 'UnaryExpression', 139 | operator: '!', 140 | argument: { 141 | type: 'MemberExpression', 142 | object: { 143 | type: 'Identifier', 144 | name: 'Meteor', 145 | }, 146 | property: { 147 | type: 'Identifier', 148 | name: 'isClient', 149 | }, 150 | }, 151 | }, 152 | consequent, 153 | }, 154 | consequent, 155 | ] 156 | ); 157 | assert.equal(result.size, 1); 158 | assert.ok(result.has('server')); 159 | }); 160 | 161 | it('ignores unresolvable IfStatements is in ancestors', () => { 162 | const consequent = { type: 'BlockStatement' }; 163 | const result = filterExecutorsByAncestors(new Set(['browser', 'server']), [ 164 | { type: 'Program' }, 165 | { 166 | type: 'IfStatement', 167 | test: { type: 'Identifier' }, 168 | consequent, 169 | }, 170 | consequent, 171 | ]); 172 | assert.equal(result.size, 2); 173 | assert.ok(result.has('browser')); 174 | assert.ok(result.has('server')); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /tests/lib/util/executors/getExecutors.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const getExecutors = require('../../../../lib/util/executors/getExecutors'); 3 | const { UNIVERSAL } = require('../../../../lib/util/environment'); 4 | 5 | describe('getExecutors', () => { 6 | it('returns executors for no ancestors', () => { 7 | const result = getExecutors(UNIVERSAL, []); 8 | assert.equal(result.size, 3); 9 | assert.ok(result.has('server')); 10 | assert.ok(result.has('browser')); 11 | assert.ok(result.has('cordova')); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/lib/util/executors/getExecutorsByEnv.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const getExecutorsByEnv = require('../../../../lib/util/executors/getExecutorsByEnv'); 3 | 4 | const { 5 | PUBLIC, 6 | PRIVATE, 7 | CLIENT, 8 | SERVER, 9 | PACKAGE, 10 | TEST, 11 | NODE_MODULE, 12 | UNIVERSAL, 13 | PACKAGE_CONFIG, 14 | MOBILE_CONFIG, 15 | COMPATIBILITY, 16 | NON_METEOR, 17 | } = require('../../../../lib/util/environment'); 18 | 19 | describe('getExecutorsByEnv', () => { 20 | it('public', () => { 21 | const result = getExecutorsByEnv(PUBLIC); 22 | assert.equal(result.size, 0); 23 | }); 24 | it('private', () => { 25 | const result = getExecutorsByEnv(PRIVATE); 26 | assert.equal(result.size, 0); 27 | }); 28 | it('client', () => { 29 | const result = getExecutorsByEnv(CLIENT); 30 | assert.equal(result.size, 2); 31 | assert.ok(result.has('browser')); 32 | assert.ok(result.has('cordova')); 33 | }); 34 | it('server', () => { 35 | const result = getExecutorsByEnv(SERVER); 36 | assert.equal(result.size, 1); 37 | assert.ok(result.has('server')); 38 | }); 39 | it('package', () => { 40 | const result = getExecutorsByEnv(PACKAGE); 41 | assert.equal(result.size, 0); 42 | }); 43 | it('test', () => { 44 | const result = getExecutorsByEnv(TEST); 45 | assert.equal(result.size, 0); 46 | }); 47 | it('node_module', () => { 48 | const result = getExecutorsByEnv(NODE_MODULE); 49 | assert.equal(result.size, 0); 50 | }); 51 | it('universal', () => { 52 | const result = getExecutorsByEnv(UNIVERSAL); 53 | assert.equal(result.size, 3); 54 | assert.ok(result.has('browser')); 55 | assert.ok(result.has('server')); 56 | assert.ok(result.has('cordova')); 57 | }); 58 | it('packageConfig', () => { 59 | const result = getExecutorsByEnv(PACKAGE_CONFIG); 60 | assert.equal(result.size, 0); 61 | }); 62 | it('mobileConfig', () => { 63 | const result = getExecutorsByEnv(MOBILE_CONFIG); 64 | assert.equal(result.size, 0); 65 | }); 66 | it('compatibility', () => { 67 | const result = getExecutorsByEnv(COMPATIBILITY); 68 | assert.equal(result.size, 2); 69 | assert.ok(result.has('cordova')); 70 | assert.ok(result.has('browser')); 71 | }); 72 | it('nonMeteor', () => { 73 | const result = getExecutorsByEnv(NON_METEOR); 74 | assert.equal(result.size, 0); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/lib/util/executors/getExecutorsFromTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const getExecutorsFromTest = require('../../../../lib/util/executors/getExecutorsFromTest'); 3 | 4 | describe('getExecutorsFromTest', () => { 5 | it('throws for unkown type', () => { 6 | assert.throws(() => { 7 | getExecutorsFromTest({ 8 | type: 'Identifier', 9 | name: 'Meteor', 10 | }); 11 | }); 12 | }); 13 | 14 | describe('MemberExpression', () => { 15 | it('isClient', () => { 16 | const result = getExecutorsFromTest({ 17 | type: 'MemberExpression', 18 | object: { 19 | type: 'Identifier', 20 | name: 'Meteor', 21 | }, 22 | property: { 23 | type: 'Identifier', 24 | name: 'isClient', 25 | }, 26 | }); 27 | assert.equal(result.size, 2); 28 | assert.ok(result.has('browser')); 29 | assert.ok(result.has('cordova')); 30 | }); 31 | it('isServer', () => { 32 | const result = getExecutorsFromTest({ 33 | type: 'MemberExpression', 34 | object: { 35 | type: 'Identifier', 36 | name: 'Meteor', 37 | }, 38 | property: { 39 | type: 'Identifier', 40 | name: 'isServer', 41 | }, 42 | }); 43 | assert.equal(result.size, 1); 44 | assert.ok(result.has('server')); 45 | }); 46 | it('isCordova', () => { 47 | const result = getExecutorsFromTest({ 48 | type: 'MemberExpression', 49 | object: { 50 | type: 'Identifier', 51 | name: 'Meteor', 52 | }, 53 | property: { 54 | type: 'Identifier', 55 | name: 'isCordova', 56 | }, 57 | }); 58 | assert.equal(result.size, 1); 59 | assert.ok(result.has('cordova')); 60 | }); 61 | it('throws on unkown Meteor prop', () => { 62 | assert.throws(() => { 63 | getExecutorsFromTest({ 64 | type: 'MemberExpression', 65 | object: { 66 | type: 'Identifier', 67 | name: 'Meteor', 68 | }, 69 | property: { 70 | type: 'Identifier', 71 | name: 'isNotAMeteorProp', 72 | }, 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('LogicalExpression', () => { 79 | it('resolves isServer AND isClient', () => { 80 | const result = getExecutorsFromTest({ 81 | type: 'LogicalExpression', 82 | operator: '&&', 83 | left: { 84 | type: 'MemberExpression', 85 | object: { 86 | type: 'Identifier', 87 | name: 'Meteor', 88 | }, 89 | property: { 90 | type: 'Identifier', 91 | name: 'isServer', 92 | }, 93 | }, 94 | right: { 95 | type: 'MemberExpression', 96 | object: { 97 | type: 'Identifier', 98 | name: 'Meteor', 99 | }, 100 | property: { 101 | type: 'Identifier', 102 | name: 'isClient', 103 | }, 104 | }, 105 | }); 106 | assert.equal(result.size, 0); 107 | }); 108 | 109 | it('resolves isServer OR isClient', () => { 110 | const result = getExecutorsFromTest({ 111 | type: 'LogicalExpression', 112 | operator: '||', 113 | left: { 114 | type: 'MemberExpression', 115 | object: { 116 | type: 'Identifier', 117 | name: 'Meteor', 118 | }, 119 | property: { 120 | type: 'Identifier', 121 | name: 'isServer', 122 | }, 123 | }, 124 | right: { 125 | type: 'MemberExpression', 126 | object: { 127 | type: 'Identifier', 128 | name: 'Meteor', 129 | }, 130 | property: { 131 | type: 'Identifier', 132 | name: 'isClient', 133 | }, 134 | }, 135 | }); 136 | assert.equal(result.size, 3); 137 | assert.ok(result.has('browser')); 138 | assert.ok(result.has('server')); 139 | assert.ok(result.has('cordova')); 140 | }); 141 | 142 | it('throws for unkown operator in LogicalExpression', () => { 143 | assert.throws(() => { 144 | getExecutorsFromTest({ 145 | type: 'LogicalExpression', 146 | operator: 'XY', 147 | left: {}, 148 | right: {}, 149 | }); 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /tests/lib/util/executors/isMeteorBlockOnlyTest.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const isMeteorBlockOnlyTest = require('../../../../lib/util/executors/isMeteorBlockOnlyTest'); 3 | 4 | describe('isMeteorBlockOnlyTest', () => { 5 | it('accepts a valid MemberExpression', () => { 6 | const result = isMeteorBlockOnlyTest({ 7 | type: 'MemberExpression', 8 | object: { 9 | type: 'Identifier', 10 | name: 'Meteor', 11 | }, 12 | property: { 13 | type: 'Identifier', 14 | name: 'isClient', 15 | }, 16 | }); 17 | assert.ok(result); 18 | }); 19 | 20 | it('accepts a valid computed MemberExpression', () => { 21 | const result = isMeteorBlockOnlyTest({ 22 | type: 'MemberExpression', 23 | computed: true, 24 | object: { 25 | type: 'Identifier', 26 | name: 'Meteor', 27 | }, 28 | property: { 29 | type: 'Literal', 30 | value: 'isCordova', 31 | }, 32 | }); 33 | assert.ok(result); 34 | }); 35 | 36 | it('does not accept an invalid MemberExpression', () => { 37 | const result = isMeteorBlockOnlyTest({ 38 | type: 'MemberExpression', 39 | object: { 40 | type: 'Identifier', 41 | name: 'Foo', 42 | }, 43 | property: { 44 | type: 'Identifier', 45 | name: 'isClient', 46 | }, 47 | }); 48 | assert.ok(!result); 49 | }); 50 | 51 | it('accepts a valid UnaryExpression', () => { 52 | const result = isMeteorBlockOnlyTest({ 53 | type: 'UnaryExpression', 54 | operator: '!', 55 | argument: { 56 | type: 'MemberExpression', 57 | object: { 58 | type: 'Identifier', 59 | name: 'Meteor', 60 | }, 61 | property: { 62 | type: 'Identifier', 63 | name: 'isServer', 64 | }, 65 | }, 66 | }); 67 | assert.ok(result); 68 | }); 69 | 70 | it('does not accept an invalid UnaryExpression', () => { 71 | const result = isMeteorBlockOnlyTest({ 72 | type: 'UnaryExpression', 73 | operator: '!', 74 | argument: { 75 | type: 'MemberExpression', 76 | object: { 77 | type: 'Identifier', 78 | name: 'Foo', 79 | }, 80 | property: { 81 | type: 'Identifier', 82 | name: 'isClient', 83 | }, 84 | }, 85 | }); 86 | assert.ok(!result); 87 | }); 88 | 89 | it('accepts a valid LogicalExpression', () => { 90 | const result = isMeteorBlockOnlyTest({ 91 | type: 'LogicalExpression', 92 | operator: '||', 93 | left: { 94 | type: 'LogicalExpression', 95 | operator: '&&', 96 | left: { 97 | type: 'MemberExpression', 98 | object: { 99 | type: 'Identifier', 100 | name: 'Meteor', 101 | }, 102 | property: { 103 | type: 'Identifier', 104 | name: 'isClient', 105 | }, 106 | }, 107 | right: { 108 | type: 'MemberExpression', 109 | object: { 110 | type: 'Identifier', 111 | name: 'Meteor', 112 | }, 113 | property: { 114 | type: 'Identifier', 115 | name: 'isServer', 116 | }, 117 | }, 118 | }, 119 | right: { 120 | type: 'MemberExpression', 121 | object: { 122 | type: 'Identifier', 123 | name: 'Meteor', 124 | }, 125 | property: { 126 | type: 'Identifier', 127 | name: 'isCordova', 128 | }, 129 | }, 130 | }); 131 | assert.ok(result); 132 | }); 133 | 134 | it('does not accept an invalid LogicalExpression', () => { 135 | const result = isMeteorBlockOnlyTest({ 136 | type: 'LogicalExpression', 137 | operator: '||', 138 | left: { 139 | type: 'LogicalExpression', 140 | operator: '&&', 141 | left: { 142 | type: 'MemberExpression', 143 | object: { 144 | type: 'Identifier', 145 | name: 'Foo', 146 | }, 147 | property: { 148 | type: 'Identifier', 149 | name: 'isClient', 150 | }, 151 | }, 152 | right: { 153 | type: 'MemberExpression', 154 | object: { 155 | type: 'Identifier', 156 | name: 'Meteor', 157 | }, 158 | property: { 159 | type: 'Identifier', 160 | name: 'isServer', 161 | }, 162 | }, 163 | }, 164 | right: { 165 | type: 'MemberExpression', 166 | object: { 167 | type: 'Identifier', 168 | name: 'Meteor', 169 | }, 170 | property: { 171 | type: 'Identifier', 172 | name: 'isCordova', 173 | }, 174 | }, 175 | }); 176 | assert.ok(!result); 177 | }); 178 | 179 | it('returns false for unresolvable expressions', () => { 180 | const result = isMeteorBlockOnlyTest({ type: 'Identifier' }); 181 | assert.ok(!result); 182 | }); 183 | 184 | it('returns false for invalid unary expressions', () => { 185 | const result = isMeteorBlockOnlyTest({ 186 | type: 'UnaryExpression', 187 | operator: '-', 188 | argument: { 189 | type: 'MemberExpression', 190 | object: { 191 | type: 'Identifier', 192 | name: 'Foo', 193 | }, 194 | property: { 195 | type: 'Identifier', 196 | name: 'isClient', 197 | }, 198 | }, 199 | }); 200 | assert.ok(!result); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /tests/lib/util/executors/sets.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { 3 | difference, 4 | union, 5 | intersection, 6 | } = require('../../../../lib/util/executors/sets'); 7 | 8 | describe('executors', () => { 9 | describe('union', () => { 10 | it('unifies two sets', () => { 11 | const result = union(new Set(['cordova']), new Set(['client', 'server'])); 12 | assert.equal(result.size, 3); 13 | assert.ok(result.has('client')); 14 | assert.ok(result.has('cordova')); 15 | assert.ok(result.has('server')); 16 | }); 17 | }); 18 | 19 | describe('difference', () => { 20 | it('returns the difference when b contains nothing from a', () => { 21 | const result = difference( 22 | new Set(['cordova']), 23 | new Set(['client', 'server']) 24 | ); 25 | assert.equal(result.size, 1); 26 | assert.ok(result.has('cordova')); 27 | }); 28 | 29 | it('returns the difference when b contains one value from a', () => { 30 | const result = difference( 31 | new Set(['client', 'cordova']), 32 | new Set(['client', 'server']) 33 | ); 34 | assert.equal(result.size, 1); 35 | assert.ok(result.has('cordova')); 36 | }); 37 | }); 38 | 39 | describe('intersection', () => { 40 | it('returns the intersection', () => { 41 | const result = intersection( 42 | new Set(['client', 'cordova']), 43 | new Set(['client', 'server']) 44 | ); 45 | assert.equal(result.size, 1); 46 | assert.ok(result.has('client')); 47 | }); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------