Skip to content

Commit cfd1c34

Browse files
authored
Merge pull request #1509 from jomasti/issue-1435
Add new rule for preventing `this` in SFCs
2 parents 06e1667 + efbb8a5 commit cfd1c34

File tree

6 files changed

+406
-12
lines changed

6 files changed

+406
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Enable the rules that you would like to use.
114114
* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
115115
* [react/no-typos](docs/rules/no-typos.md): Prevent common casing typos
116116
* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
117+
* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components
117118
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
118119
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
119120
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types

docs/rules/no-this-in-sfc.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Prevent `this` from being used in stateless functional components (react/no-this-in-sfc)
2+
3+
When using a stateless functional component (SFC), props/context aren't accessed in the same way as a class component or the `create-react-class` format. Both props and context are passed as separate arguments to the component instead. Also, as the name suggests, a stateless component does not have state on `this.state`.
4+
5+
Attempting to access properties on `this` can be a potential error if someone is unaware of the differences when writing a SFC or missed when converting a class component to a SFC.
6+
7+
8+
## Rule Details
9+
10+
The following patterns are considered warnings:
11+
12+
```jsx
13+
function Foo(props) {
14+
return (
15+
<div>{this.props.bar}</div>
16+
);
17+
}
18+
```
19+
20+
```jsx
21+
function Foo(props) {
22+
const { bar } = this.props;
23+
return (
24+
<div>{bar}</div>
25+
);
26+
}
27+
```
28+
29+
```jsx
30+
function Foo(props, context) {
31+
return (
32+
<div>
33+
{this.context.foo ? this.props.bar : ''}
34+
</div>
35+
);
36+
}
37+
```
38+
39+
```jsx
40+
function Foo(props, context) {
41+
const { foo } = this.context;
42+
const { bar } = this.props;
43+
return (
44+
<div>
45+
{foo ? bar : ''}
46+
</div>
47+
);
48+
}
49+
```
50+
51+
```jsx
52+
function Foo(props) {
53+
if (this.state.loading) {
54+
return <Loader />;
55+
}
56+
return (
57+
<div>
58+
{this.props.bar}
59+
</div>
60+
);
61+
}
62+
```
63+
64+
```jsx
65+
function Foo(props) {
66+
const { loading } = this.state;
67+
const { bar } = this.props;
68+
if (loading) {
69+
return <Loader />;
70+
}
71+
return (
72+
<div>
73+
{bar}
74+
</div>
75+
);
76+
}
77+
```
78+
79+
The following patterns are **not** considered warnings:
80+
81+
```jsx
82+
function Foo(props) {
83+
return (
84+
<div>{props.bar}</div>
85+
);
86+
}
87+
```
88+
89+
```jsx
90+
function Foo(props) {
91+
const { bar } = props;
92+
return (
93+
<div>{bar}</div>
94+
);
95+
}
96+
```
97+
98+
```jsx
99+
function Foo({ bar }) {
100+
return (
101+
<div>{bar}</div>
102+
);
103+
}
104+
```
105+
106+
```jsx
107+
function Foo(props, context) {
108+
return (
109+
<div>
110+
{context.foo ? props.bar : ''}
111+
</div>
112+
);
113+
}
114+
```
115+
116+
```jsx
117+
function Foo(props, context) {
118+
const { foo } = context;
119+
const { bar } = props;
120+
return (
121+
<div>
122+
{foo ? bar : ''}
123+
</div>
124+
);
125+
}
126+
```
127+
128+
```jsx
129+
function Foo({ bar }, { foo }) {
130+
return (
131+
<div>
132+
{foo ? bar : ''}
133+
</div>
134+
);
135+
}
136+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const allRules = {
5757
'no-string-refs': require('./lib/rules/no-string-refs'),
5858
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
5959
'no-render-return-value': require('./lib/rules/no-render-return-value'),
60+
'no-this-in-sfc': require('./lib/rules/no-this-in-sfc'),
6061
'no-typos': require('./lib/rules/no-typos'),
6162
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
6263
'no-unknown-property': require('./lib/rules/no-unknown-property'),

lib/rules/no-this-in-sfc.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @fileoverview Report "this" being used in stateless functional components.
3+
*/
4+
'use strict';
5+
6+
const Components = require('../util/Components');
7+
8+
// ------------------------------------------------------------------------------
9+
// Constants
10+
// ------------------------------------------------------------------------------
11+
12+
const ERROR_MESSAGE = 'Stateless functional components should not use this';
13+
14+
// ------------------------------------------------------------------------------
15+
// Rule Definition
16+
// ------------------------------------------------------------------------------
17+
18+
module.exports = {
19+
meta: {
20+
docs: {
21+
description: 'Report "this" being used in stateless components',
22+
category: 'Possible Errors',
23+
recommended: false
24+
},
25+
schema: []
26+
},
27+
28+
create: Components.detect((context, components, utils) => ({
29+
MemberExpression(node) {
30+
const component = components.get(utils.getParentStatelessComponent());
31+
if (!component) {
32+
return;
33+
}
34+
if (node.object.type === 'ThisExpression') {
35+
context.report({
36+
node: node,
37+
message: ERROR_MESSAGE
38+
});
39+
}
40+
}
41+
}))
42+
};

lib/util/Components.js

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,7 @@ function componentRule(rule, context) {
303303
return calledOnReact;
304304
},
305305

306-
/**
307-
* Check if the node is returning JSX
308-
*
309-
* @param {ASTNode} ASTnode The AST node being checked
310-
* @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases
311-
* @returns {Boolean} True if the node is returning JSX, false if not
312-
*/
313-
isReturningJSX: function(ASTnode, strict) {
306+
getReturnPropertyAndNode(ASTnode) {
314307
let property;
315308
let node = ASTnode;
316309
switch (node.type) {
@@ -319,14 +312,36 @@ function componentRule(rule, context) {
319312
break;
320313
case 'ArrowFunctionExpression':
321314
property = 'body';
315+
if (node[property] && node[property].type === 'BlockStatement') {
316+
node = utils.findReturnStatement(node);
317+
property = 'argument';
318+
}
322319
break;
323320
default:
324321
node = utils.findReturnStatement(node);
325-
if (!node) {
326-
return false;
327-
}
328322
property = 'argument';
329323
}
324+
return {
325+
node: node,
326+
property: property
327+
};
328+
},
329+
330+
/**
331+
* Check if the node is returning JSX
332+
*
333+
* @param {ASTNode} ASTnode The AST node being checked
334+
* @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases
335+
* @returns {Boolean} True if the node is returning JSX, false if not
336+
*/
337+
isReturningJSX: function(ASTnode, strict) {
338+
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
339+
const node = nodeAndProperty.node;
340+
const property = nodeAndProperty.property;
341+
342+
if (!node) {
343+
return false;
344+
}
330345

331346
const returnsConditionalJSXConsequent =
332347
node[property] &&
@@ -356,6 +371,35 @@ function componentRule(rule, context) {
356371
);
357372
},
358373

374+
/**
375+
* Check if the node is returning null
376+
*
377+
* @param {ASTNode} ASTnode The AST node being checked
378+
* @returns {Boolean} True if the node is returning null, false if not
379+
*/
380+
isReturningNull(ASTnode) {
381+
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
382+
const property = nodeAndProperty.property;
383+
const node = nodeAndProperty.node;
384+
385+
if (!node) {
386+
return false;
387+
}
388+
389+
return node[property] && node[property].value === null;
390+
},
391+
392+
/**
393+
* Check if the node is returning JSX or null
394+
*
395+
* @param {ASTNode} ASTnode The AST node being checked
396+
* @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases
397+
* @returns {Boolean} True if the node is returning JSX or null, false if not
398+
*/
399+
isReturningJSXOrNull(ASTNode, strict) {
400+
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
401+
},
402+
359403
/**
360404
* Find a return statment in the current node
361405
*
@@ -430,7 +474,7 @@ function componentRule(rule, context) {
430474
return null;
431475
}
432476
// Return the node if it is a function that is not a class method and is not inside a JSX Element
433-
if (isFunction && !isMethod && !isJSXExpressionContainer) {
477+
if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSXOrNull(node)) {
434478
return node;
435479
}
436480
scope = scope.upper;

0 commit comments

Comments
 (0)