Skip to content

Commit c48dcfd

Browse files
committed
Add lint rule
From #3835
1 parent bb42ad9 commit c48dcfd

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ module.exports = {
169169
'rulesdir/sort-imports': [ERROR],
170170
'rulesdir/imports': [ERROR],
171171
'rulesdir/useLayoutEffectRule': [ERROR],
172+
'rulesdir/pure-render': [ERROR],
172173

173174
// jsx-a11y rules
174175
'jsx-a11y/accessible-emoji': ERROR,

bin/pure-render.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2020 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
// Copied from https://github.com/facebook/react/pull/24506
14+
module.exports = {
15+
meta: {
16+
type: 'problem',
17+
docs: {
18+
description: 'enforces component render purity',
19+
recommended: true,
20+
url: 'https://beta.reactjs.org/learn/keeping-components-pure'
21+
}
22+
},
23+
create(context) {
24+
return {
25+
MemberExpression(member) {
26+
// Look for member expressions that look like refs (i.e. `ref.current`).
27+
if (
28+
member.object.type !== 'Identifier' ||
29+
member.computed ||
30+
member.property.type !== 'Identifier' ||
31+
member.property.name !== 'current'
32+
) {
33+
return;
34+
}
35+
36+
// Find the parent function of this node, as well as any if statement matching against the ref value
37+
// (i.e. lazy init pattern shown in React docs).
38+
let node = member;
39+
let fn;
40+
let conditional;
41+
while (node) {
42+
if (
43+
node.type === 'FunctionDeclaration' ||
44+
node.type === 'FunctionExpression' ||
45+
node.type === 'ArrowFunctionExpression'
46+
) {
47+
fn = node;
48+
break;
49+
}
50+
51+
if (
52+
node.type === 'IfStatement' &&
53+
node.test.type === 'BinaryExpression' &&
54+
(node.test.operator === '==' || node.test.operator === '===') &&
55+
isMemberExpressionEqual(node.test.left, member)
56+
) {
57+
conditional = node.test;
58+
}
59+
60+
node = node.parent;
61+
}
62+
63+
if (!fn) {
64+
return;
65+
}
66+
67+
// Find the variable definition for the object.
68+
const variable = getVariable(context.getScope(), member.object.name);
69+
if (!variable) {
70+
return;
71+
}
72+
73+
// Find the initialization of the variable and see if it's a call to useRef.
74+
const refDefinition = variable.defs.find(def => {
75+
const init = def.node.init;
76+
if (!init) {
77+
return false;
78+
}
79+
80+
return (
81+
init.type === 'CallExpression' &&
82+
((init.callee.type === 'Identifier' &&
83+
init.callee.name === 'useRef') ||
84+
(init.callee.type === 'MemberExpression' &&
85+
init.callee.object.type === 'Identifier' &&
86+
init.callee.object.name === 'React' &&
87+
init.callee.property.type === 'Identifier' &&
88+
init.callee.property.name === 'useRef')) &&
89+
parentFunction(def.node) === fn
90+
);
91+
});
92+
93+
if (refDefinition) {
94+
// If within an if statement, check if comparing with the initial value passed to useRef.
95+
// This indicates the lazy init pattern, which is allowed.
96+
if (conditional) {
97+
const init = refDefinition.node.init.arguments[0] || {
98+
type: 'Identifier',
99+
name: 'undefined'
100+
};
101+
if (isLiteralEqual(conditional.operator, init, conditional.right)) {
102+
return;
103+
}
104+
}
105+
106+
// Otherwise, report an error for either writing or reading to this ref based on parent expression.
107+
context.report({
108+
node: member,
109+
message:
110+
member.parent.type === 'AssignmentExpression' &&
111+
member.parent.left === member
112+
? 'Writing to refs during rendering is not allowed. Move this into a useEffect or useLayoutEffect. See https://beta.reactjs.org/apis/useref'
113+
: 'Reading from refs during rendering is not allowed. See https://beta.reactjs.org/apis/useref'
114+
});
115+
}
116+
}
117+
};
118+
}
119+
};
120+
121+
function getVariable(scope, name) {
122+
while (scope) {
123+
const variable = scope.set.get(name);
124+
if (variable) {
125+
return variable;
126+
}
127+
128+
scope = scope.upper;
129+
}
130+
}
131+
132+
function parentFunction(node) {
133+
while (node) {
134+
if (
135+
node.type === 'FunctionDeclaration' ||
136+
node.type === 'FunctionExpression' ||
137+
node.type === 'ArrowFunctionExpression'
138+
) {
139+
return node;
140+
}
141+
142+
node = node.parent;
143+
}
144+
}
145+
146+
function isMemberExpressionEqual(a, b) {
147+
if (a === b) {
148+
return true;
149+
}
150+
151+
return (
152+
a.type === 'MemberExpression' &&
153+
b.type === 'MemberExpression' &&
154+
a.object.type === 'Identifier' &&
155+
b.object.type === 'Identifier' &&
156+
a.object.name === b.object.name &&
157+
a.property.type === 'Identifier' &&
158+
b.property.type === 'Identifier' &&
159+
a.property.name === b.property.name
160+
);
161+
}
162+
163+
function isLiteralEqual(operator, a, b) {
164+
let aValue, bValue;
165+
if (a.type === 'Identifier' && a.name === 'undefined') {
166+
aValue = undefined;
167+
} else if (a.type === 'Literal') {
168+
aValue = a.value;
169+
} else {
170+
return;
171+
}
172+
173+
if (b.type === 'Identifier' && b.name === 'undefined') {
174+
bValue = undefined;
175+
} else if (b.type === 'Literal') {
176+
bValue = b.value;
177+
} else {
178+
return;
179+
}
180+
181+
if (operator === '===') {
182+
return aValue === bValue;
183+
} else if (operator === '==') {
184+
// eslint-disable-next-line
185+
return aValue == bValue;
186+
}
187+
188+
return false;
189+
}

0 commit comments

Comments
 (0)