├── .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 | [](https://badge.fury.io/js/eslint-plugin-no-react-scope-bound-assignment)
4 | [](https://travis-ci.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment)
5 | [](https://david-dm.org/betaorbust/eslint-plugin-no-react-scope-bound-assignment/status.svg)
6 | [](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 | });
--------------------------------------------------------------------------------