├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── docs └── no-react-scope-bound-assignment.md ├── index.js ├── package.json ├── rules └── no-react-scope-bound-assignment.js └── tests ├── fixtures ├── failure.jsx └── success.jsx └── no-react-scope-bound-assignment.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | }, 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "globals": { 10 | "module": true, 11 | "exports": true, 12 | "require": true 13 | }, 14 | "rules": { 15 | // possible errors 16 | "comma-dangle": [ 2 ], 17 | "no-cond-assign": [ 2 ], 18 | "no-console": [ 2 ], 19 | "no-constant-condition": [ 2 ], 20 | "no-control-regex": [ 2 ], 21 | "no-debugger": [ 2 ], 22 | "no-dupe-args": [ 2 ], 23 | "no-dupe-keys": [ 2 ], 24 | "no-duplicate-case": [ 2 ], 25 | "no-empty": [ 2 ], 26 | "no-empty-character-class": [ 2 ], 27 | "no-ex-assign": [ 2 ], 28 | "no-extra-boolean-cast": [ 2 ], 29 | "no-extra-semi": [ 2 ], 30 | "no-func-assign": [ 2 ], 31 | "no-inner-declarations": [ 2, "both" ], 32 | "no-invalid-regexp": [ 2 ], 33 | "no-irregular-whitespace": [ 2 ], 34 | "no-negated-in-lhs": [ 2 ], 35 | // when IE8 dies 36 | "no-reserved-keys": [ 0 ], 37 | "no-regex-spaces": [ 2 ], 38 | "no-sparse-arrays": [ 2 ], 39 | "no-unreachable": [ 2 ], 40 | "use-isnan": [ 2 ], 41 | "valid-typeof": [ 2 ], 42 | 43 | // best practices 44 | "block-scoped-var": [ 2 ], 45 | "consistent-return": [ 2 ], 46 | "curly": [ 2 ], 47 | "default-case": [ 2 ], 48 | "dot-notation": [ 2, { "allowKeywords": true } ], 49 | "eqeqeq": [ 2 ], 50 | "guard-for-in": [ 2 ], 51 | "no-alert": [ 2 ], 52 | "no-caller": [ 2 ], 53 | "no-div-regex": [ 2 ], 54 | "no-eq-null": [ 2 ], 55 | "no-eval": [ 2 ], 56 | "no-extend-native": [ 2 ], 57 | "no-extra-bind": [ 2 ], 58 | "no-fallthrough": [ 2 ], 59 | "no-floating-decimal": [ 2 ], 60 | "no-implied-eval": [ 2 ], 61 | "no-iterator": [ 2 ], 62 | "no-labels": [ 2 ], 63 | "no-lone-blocks": [ 2 ], 64 | "no-loop-func": [ 2 ], 65 | "no-multi-spaces": [ 0 ], 66 | "no-native-reassign": [ 2 ], 67 | "no-new": [ 2 ], 68 | "no-new-func": [ 2 ], 69 | "no-new-wrappers": [ 2 ], 70 | "no-octal": [ 2 ], 71 | "no-octal-escape": [ 2 ], 72 | "no-param-reassign": [ 2 ], 73 | "no-proto": [ 2 ], 74 | "no-redeclare": [ 2 ], 75 | "no-return-assign": [ 2 ], 76 | "no-script-url": [ 2 ], 77 | "no-self-compare": [ 2 ], 78 | "no-sequences": [ 2 ], 79 | "no-throw-literal": [ 2 ], 80 | "no-unused-expressions": [ 2 ], 81 | "no-void": [ 2 ], 82 | "no-with": [ 2 ], 83 | // "vars-on-top": [ 2 ], 84 | "wrap-iife": [ 2 ], 85 | "yoda": [ 0 ], 86 | 87 | // strict mode 88 | "strict": [ 2, "global" ], 89 | 90 | // variables 91 | "no-catch-shadow": [ 2 ], 92 | "no-delete-var": [ 2 ], 93 | "no-shadow": [ 2 ], 94 | "no-shadow-restricted-names": [ 2 ], 95 | "no-undef": [ 2 ], 96 | "no-undef-init": [ 2 ], 97 | "no-undefined": [ 0 ], 98 | "no-unused-vars": [ 2, { "vars": "all", "args": "none" } ], 99 | "no-use-before-define": [ 2, "nofunc" ], 100 | 101 | // node.js 102 | "handle-callback-err": [ 2, "^.*(e|E)rr" ], 103 | "no-mixed-requires": [ 2 ], 104 | "no-new-require": [ 2 ], 105 | "no-restricted-modules": [ 2, "" ], 106 | "no-process-exit": [ 0 ], 107 | 108 | // ES6 109 | "generator-star-spacing": [ 2, "after" ], 110 | 111 | // stylistic 112 | "new-cap": [ 2, {"capIsNewExceptions": ["URI"]}], 113 | "camelcase": [ 2, { "properties": "never" } ], 114 | "eol-last": [ 0 ], 115 | "key-spacing": [ 0 ], 116 | "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ], 117 | "no-nested-ternary": [ 2 ], 118 | "no-underscore-dangle": [ 0 ], 119 | "semi": [ 2, "always" ], 120 | "space-infix-ops": [ 2 ], 121 | "quotes": [ 0 ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ## Remove everything 2 | * 3 | **/* 4 | 5 | ## Whitelist our rules file along with our entry index. 6 | !rules/**/* 7 | !index.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "6" 5 | - "5" 6 | - "4" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Disallow assignment of scope-bound variables from within React classes (no-react-scope-bound-assignment) 2 | ======================================================================================================== 3 | [![NPM Version](https://badge.fury.io/js/eslint-plugin-no-react-scope-bound-assignment.svg)](https://badge.fury.io/js/eslint-plugin-no-react-scope-bound-assignment) 4 | [![Build Status](https://travis-ci.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment.svg?branch=master)](https://travis-ci.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment) 5 | [![Dependency Status](https://david-dm.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment/status.svg)](https://david-dm.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment/status.svg) 6 | [![Dev Dependency Status](https://david-dm.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment/dev-status.svg)](https://david-dm.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment/dev-status.svg) 7 | 8 | Variables declared outside of a React.createClass call are scope bound inside that call. If the variable is assigned 9 | or reassigned from within the React class, this value is shared across all instances of the class, but more importantly, 10 | when isomporphically rendering in Node, the variable will be shared across all renders, as Node will require the 11 | file only once. 12 | 13 | For more information on the rule itself, see the [rule doc](docs/no-react-scope-bound-assignment.md). 14 | -------------------------------------------------------------------------------- /docs/no-react-scope-bound-assignment.md: -------------------------------------------------------------------------------- 1 | # Disallow assignment of scope-bound variables from within React classes (no-react-scope-bound-assignment) 2 | 3 | Variables declared outside of a React.createClass call are scope bound inside that call. If the variable is assigned 4 | or reassigned from within the React class, this value is shared across all instances of the class, but more importantly, 5 | when isomporphically rendering in Node, the variable will be shared across all renders, as Node will require the 6 | file only once. 7 | 8 | This is a nasty little bug that can cause a ton of pain, and this rule will help you not fall into this specific trap. 9 | 10 | 11 | ## Rule Details 12 | This rule is aimed at eliminating errors and silent defects in code by ensuring that variables are not reassigned 13 | after being scope bound into a React component class. 14 | 15 | The following are considered warnings: 16 | 17 | ```js 18 | var a = 'Leela'; 19 | React.createClass({ 20 | render: { 21 | if(a === 'Leela'){ 22 | a = 'Fry'; 23 | } 24 | return (
{{a}}
); 25 | } 26 | }); 27 | ``` 28 | 29 | The following pattern is not considered a warning: 30 | 31 | ```js 32 | var a = 'Leela'; 33 | if(a === 'Leela'){ 34 | a = 'Fry'; 35 | } 36 | React.createClass({ 37 | render: { 38 | return (
{{a}}
); 39 | } 40 | }); 41 | ``` 42 | 43 | ## Options 44 | 45 | This rule takes no options. 46 | 47 | ## When not to use it 48 | 49 | If you want to do some ill-advised in-memory latching on a per-server basis? -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | rules: { 4 | 'no-react-scope-bound-assignment': require('./rules/no-react-scope-bound-assignment') 5 | }, 6 | rulseConfig: { 7 | 'no-react-scope-bound-assignment': 2 8 | } 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-no-react-scope-bound-assignment", 3 | "description": "EsLint rule to disallow assigning scope-bound variables from within React components", 4 | "version": "1.0.2", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests", 8 | "docs": "docs" 9 | }, 10 | "scripts": { 11 | "test": "mocha tests" 12 | }, 13 | "keywords": [ 14 | "eslint", 15 | "eslintplugin" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/betaorbust/eslint-plugin-no-react-scope-bound-assignment" 20 | }, 21 | "author": "Jacques Favreau", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "eslint": "^3.5.0", 25 | "mocha": "^3.0.2" 26 | }, 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /rules/no-react-scope-bound-assignment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @fileoverview Rule to flag writing to constant variables or variables named like constants. 5 | * @author Jacques Favreau 6 | */ 7 | //------------------------------------------------------------------------------ 8 | // Rule Definition 9 | //------------------------------------------------------------------------------ 10 | function noJsxScopeBoundWrites(context) { 11 | //-------------------------------------------------------------------------- 12 | // Helpers 13 | // -------------------------------------------------------------------------- 14 | 15 | var WARNING_MESSAGE = '{{name}} is initialized outside of a react class, but reassigned within one.'; 16 | 17 | /** 18 | * Assigns references to corresponding variable 19 | * 20 | * @param {Scope} scope An escope object 21 | * @param {Variable} variable Variable to apply references too 22 | * @returns {Variable} returns the variable with references 23 | * @private 24 | */ 25 | function transformGlobalVariables(scope, variable) { 26 | if (variable.references.length === 0) { 27 | scope.references.forEach(function(ref) { 28 | if (ref.identifier.name === variable.name) { 29 | variable.references.push(ref); 30 | } 31 | }); 32 | } 33 | 34 | return variable; 35 | } 36 | 37 | /** 38 | * Check if a node is a unary delete expression 39 | * 40 | * @param {ASTNode} node The node to compare 41 | * @returns {boolean} True if it's a unary delete expression, false if not. 42 | * @private 43 | */ 44 | function isUnaryDelete(node) { 45 | return ( 46 | node && 47 | node.type === "UnaryExpression" && 48 | node.operator === "delete" && 49 | node.argument.type === "Identifier" 50 | ); 51 | } 52 | 53 | /** 54 | * Determines if the reference should be counted as a re-assignment 55 | * 56 | * @param {Reference} ref The reference to check. 57 | * @returns {boolean} True if it"s a valid reassignment, false if not. 58 | * @private 59 | */ 60 | function isReassignment(ref) { 61 | var isWrite = (ref.isWrite() || !ref.isReadOnly()); 62 | 63 | if (!isWrite && isUnaryDelete(ref.identifier.parent)) { 64 | isWrite = true; 65 | } 66 | 67 | return isWrite; 68 | } 69 | 70 | 71 | function insideReactCreateClass(ref){ 72 | if(ref.type === 'CallExpression' && 73 | ref.callee.type === 'MemberExpression' && 74 | ref.callee.object && ref.callee.object.name === 'React' && 75 | ref.callee.property && ref.callee.property.name === 'createClass'){ 76 | return true; 77 | } 78 | 79 | if(ref.identifier && ref.identifier.parent){ 80 | return insideReactCreateClass(ref.identifier.parent); 81 | }else if(ref.parent){ 82 | if(ref.parent.type && ref.parent.type === 'Program'){ 83 | return false; 84 | } 85 | return insideReactCreateClass(ref.parent); 86 | } 87 | } 88 | 89 | function checkScope(scope) { 90 | var variables = scope.variables; 91 | if (!scope.functionExpressionScope) { 92 | variables.forEach(function(variable) { 93 | if ((scope.type === 'function' && 94 | variable.name === 'arguments' && 95 | variable.identifiers.length === 0) || 96 | (!variable.defs[0])) { 97 | // Ignore implicit arguments variables and global environment variables 98 | return; 99 | } 100 | 101 | var references = variable.references; 102 | var name = variable.name; 103 | var assignments = references.filter(isReassignment); 104 | if(assignments.length > 1){ 105 | assignments.shift(); 106 | assignments.forEach(function(ref){ 107 | if(insideReactCreateClass(ref)){ 108 | context.report(ref.identifier, WARNING_MESSAGE, {name: name}); 109 | } 110 | }); 111 | } 112 | }); 113 | } 114 | } 115 | 116 | //-------------------------------------------------------------------------- 117 | // Public API 118 | //-------------------------------------------------------------------------- 119 | return { 120 | 'Program:exit': function(node) { 121 | var scope = context.getScope(); 122 | // https://github.com/estools/escope/issues/56 123 | if (scope.type === 'global') { 124 | scope = { 125 | childScopes: scope.childScopes, 126 | variables: scope.variables.map(transformGlobalVariables.bind(null, scope)) 127 | }; 128 | } 129 | checkScope(scope.childScopes[0]); 130 | } 131 | }; 132 | } 133 | 134 | module.exports = noJsxScopeBoundWrites; 135 | 136 | module.exports.schema = []; 137 | -------------------------------------------------------------------------------- /tests/fixtures/failure.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var React = require('react'); 3 | var outsideVariable = 'Leela'; 4 | React.createClass({ 5 | render: function() { 6 | if(outsideVariable === 'Leela'){ 7 | outsideVariable = 'Inside Value'; 8 | } 9 | return (outsideVariable); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /tests/fixtures/success.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var React = require('react'); 3 | var outsideVariable = 'Leela'; 4 | React.createClass({ 5 | render: function() { 6 | if(outsideVariable === 'Leela'){ 7 | var insideVariable = 'Inside Value'; 8 | } 9 | return insideVariable; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /tests/no-react-scope-bound-assignment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ESLintTester = require('eslint').RuleTester; 4 | var fs = require('fs'); 5 | var rule = require('../rules/no-react-scope-bound-assignment'); 6 | 7 | var eslintTester = new ESLintTester(); 8 | 9 | var parserOptions = { 10 | ecmaVersion: 6, 11 | ecmaFeatures: { jsx: true } 12 | }; 13 | 14 | var env = { node: true }; 15 | 16 | 17 | 18 | var failureCase = fs.readFileSync('./tests/fixtures/failure.jsx', 'utf8'); 19 | var successCase = fs.readFileSync('./tests/fixtures/success.jsx', 'utf8'); 20 | 21 | eslintTester.run('no-react-scope-bound-assignment', rule, { 22 | valid: [ 23 | { 24 | code: successCase, 25 | parserOptions: parserOptions, 26 | env: env 27 | } 28 | ], 29 | invalid: [ 30 | { 31 | code: failureCase, 32 | errors: [{ message: 'outsideVariable is initialized outside of a react class, but reassigned within one.' }], 33 | parserOptions: parserOptions, 34 | env: env 35 | } 36 | ] 37 | }); --------------------------------------------------------------------------------