Skip to content

Commit de6e7b5

Browse files
committed
Add support for stateless function components (fixes #237)
1 parent f4cbdfc commit de6e7b5

File tree

5 files changed

+143
-67
lines changed

5 files changed

+143
-67
lines changed

lib/rules/display-name.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ module.exports = function(context) {
156156
markDisplayNameAsDeclared(node);
157157
},
158158

159+
ReturnStatement: function(node) {
160+
componentList.set(context, node);
161+
},
162+
159163
'Program:exit': function() {
160164
var list = componentList.getList();
161165
// Report missing display name for all components

lib/rules/prop-types.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@ module.exports = function(context) {
3131
* @returns {Boolean} True if we are using a prop, false if not.
3232
*/
3333
function isPropTypesUsage(node) {
34-
return Boolean(
35-
node.object.type === 'ThisExpression' &&
36-
node.property.name === 'props'
37-
);
34+
var isClassUsage = node.object.type === 'ThisExpression' && node.property.name === 'props';
35+
var isStatelessFunctionUsage = node.object.name === 'props';
36+
return isClassUsage || isStatelessFunctionUsage;
3837
}
3938

4039
/**
@@ -314,6 +313,9 @@ module.exports = function(context) {
314313
* @return {string} the name of the property or undefined if not found
315314
*/
316315
function getPropertyName(node) {
316+
if (componentUtil.getNode(context, node)) {
317+
node = node.parent;
318+
}
317319
var property = node.property;
318320
if (property) {
319321
switch (property.type) {
@@ -351,7 +353,7 @@ module.exports = function(context) {
351353
var properties;
352354
switch (node.type) {
353355
case 'MemberExpression':
354-
name = getPropertyName(node.parent);
356+
name = getPropertyName(node);
355357
if (name) {
356358
allNames = parentNames.concat(name);
357359
if (node.parent.type === 'MemberExpression') {
@@ -397,7 +399,7 @@ module.exports = function(context) {
397399
usedPropTypes.push({
398400
name: name,
399401
allNames: allNames,
400-
node: node.parent.property
402+
node: node.object.name !== 'props' ? node.parent.property : node.property
401403
});
402404
break;
403405
case 'destructuring':
@@ -589,6 +591,10 @@ module.exports = function(context) {
589591
});
590592
},
591593

594+
ReturnStatement: function(node) {
595+
componentList.set(context, node);
596+
},
597+
592598
'Program:exit': function() {
593599
var list = componentList.getList();
594600
// Report undeclared proptypes for all classes

lib/util/component.js

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function isComponentDefinition(context, node) {
2323
break;
2424
case 'ClassDeclaration':
2525
var superClass = node.superClass && context.getSource(node.superClass);
26-
if (superClass === 'Component' && superClass === 'React.Component') {
26+
if (superClass === 'Component' || superClass === 'React.Component') {
2727
return true;
2828
}
2929
break;
@@ -34,44 +34,51 @@ function isComponentDefinition(context, node) {
3434
}
3535

3636
/**
37-
* Detect if the node is rendering some JSX
37+
* Check if we are in a stateless function component
3838
* @param {Object} context The current rule context.
3939
* @param {ASTNode} node The AST node being checked.
40-
* @returns {Boolean} True the node is rendering some JSX, false if not.
40+
* @returns {Boolean} True if we are in a stateless function component, false if not.
4141
*/
42-
function isRenderingJSX(context, node) {
43-
var tokens = context.getTokens(node);
44-
for (var i = 0, j = tokens.length; i < j; i++) {
45-
var hasJSX = /^JSX/.test(tokens[i].type);
46-
var hasReact =
47-
tokens[i].type === 'Identifier' && tokens[i].value === 'React' &&
48-
tokens[i + 2] && tokens[i + 2].type === 'Identifier' && tokens[i + 2].value === 'createElement';
49-
if (!hasJSX && !hasReact) {
50-
continue;
42+
function isStatelessFunctionComponent(context, node) {
43+
if (node.type !== 'ReturnStatement') {
44+
return false;
45+
}
46+
47+
var scope = context.getScope();
48+
while (scope) {
49+
if (scope.type === 'class') {
50+
return false;
5151
}
52-
return true;
52+
scope = scope.upper;
5353
}
54-
return false;
54+
55+
var returnsJSX =
56+
node.argument &&
57+
node.argument.type === 'JSXElement'
58+
;
59+
var returnsReactCreateElement =
60+
node.argument &&
61+
node.argument.callee &&
62+
node.argument.callee.property &&
63+
node.argument.callee.property.name === 'createElement'
64+
;
65+
66+
return Boolean(returnsJSX || returnsReactCreateElement);
5567
}
5668

5769
/**
58-
* Check if a class has a valid render method
59-
* @param {Object} context The current rule context.
60-
* @param {ASTNode} node The AST node being checked.
61-
* @returns {Boolean} True the class has a valid render method, false if not.
70+
* Get the identifiers of a React component ASTNode
71+
* @param {ASTNode} node The React component ASTNode being checked.
72+
* @returns {Object} The component identifiers.
6273
*/
63-
function isClassWithRender(context, node) {
64-
if (node.type !== 'ClassDeclaration') {
65-
return false;
66-
}
67-
for (var i = 0, j = node.body.body.length; i < j; i++) {
68-
var declaration = node.body.body[i];
69-
if (declaration.type !== 'MethodDefinition' || declaration.key.name !== 'render') {
70-
continue;
71-
}
72-
return isRenderingJSX(context, declaration);
73-
}
74-
return false;
74+
function getIdentifiers(node) {
75+
var name = node.id && node.id.name || DEFAULT_COMPONENT_NAME;
76+
var id = name + ':' + node.loc.start.line + ':' + node.loc.start.column;
77+
78+
return {
79+
id: id,
80+
name: name
81+
};
7582
}
7683

7784
/**
@@ -80,40 +87,31 @@ function isClassWithRender(context, node) {
8087
* @param {ASTNode} node The AST node being checked.
8188
* @returns {ASTNode} The ASTNode of the React component.
8289
*/
83-
function getNode(context, node) {
84-
var componentNode = null;
90+
function getNode(context, node, list) {
8591
var ancestors = context.getAncestors().reverse();
8692

8793
ancestors.unshift(node);
8894

8995
for (var i = 0, j = ancestors.length; i < j; i++) {
9096
if (isComponentDefinition(context, ancestors[i])) {
91-
componentNode = ancestors[i];
92-
break;
97+
return ancestors[i];
9398
}
94-
if (isClassWithRender(context, ancestors[i])) {
95-
componentNode = ancestors[i];
96-
break;
99+
// Node is already in the component list
100+
var identifiers = getIdentifiers(ancestors[i]);
101+
if (list && list[identifiers.id]) {
102+
return ancestors[i];
97103
}
98-
99104
}
100105

101-
return componentNode;
102-
}
103-
104-
/**
105-
* Get the identifiers of a React component ASTNode
106-
* @param {ASTNode} node The React component ASTNode being checked.
107-
* @returns {Object} The component identifiers.
108-
*/
109-
function getIdentifiers(node) {
110-
var name = node.id && node.id.name || DEFAULT_COMPONENT_NAME;
111-
var id = name + ':' + node.loc.start.line + ':' + node.loc.start.column;
106+
if (isStatelessFunctionComponent(context, node)) {
107+
var scope = context.getScope();
108+
while (scope.upper && scope.type !== 'function') {
109+
scope = scope.upper;
110+
}
111+
return scope.block;
112+
}
112113

113-
return {
114-
id: id,
115-
name: name
116-
};
114+
return null;
117115
}
118116

119117
/**
@@ -171,10 +169,11 @@ List.prototype.getList = function() {
171169
* @returns {Object} The added component.
172170
*/
173171
List.prototype.set = function(context, node, customProperties) {
174-
var componentNode = getNode(context, node);
172+
var componentNode = getNode(context, node, this._list);
175173
if (!componentNode) {
176174
return null;
177175
}
176+
178177
var identifiers = getIdentifiers(componentNode);
179178

180179
var component = util._extend({

tests/lib/rules/display-name.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ ruleTester.run('display-name', rule, {
169169
experimentalObjectRestSpread: true,
170170
jsx: true
171171
}
172+
}, {
173+
code: [
174+
'export default class {',
175+
' render() {',
176+
' return <div>Hello {this.props.name}</div>;',
177+
' }',
178+
'}'
179+
].join('\n'),
180+
parser: 'babel-eslint'
172181
}],
173182

174183
invalid: [{
@@ -237,16 +246,31 @@ ruleTester.run('display-name', rule, {
237246
}]
238247
}, {
239248
code: [
240-
'export default class {',
241-
' render() {',
242-
' return <div>Hello {this.props.name}</div>;',
243-
' }',
249+
'var Hello = function() {',
250+
' return <div>Hello {this.props.name}</div>;',
251+
'}'
252+
].join('\n'),
253+
parser: 'babel-eslint',
254+
errors: [{
255+
message: 'Component definition is missing display name'
256+
}]
257+
}, {
258+
code: [
259+
'function Hello() {',
260+
' return <div>Hello {this.props.name}</div>;',
261+
'}'
262+
].join('\n'),
263+
parser: 'babel-eslint',
264+
errors: [{
265+
message: 'Hello component definition is missing display name'
266+
}]
267+
}, {
268+
code: [
269+
'var Hello = () => {',
270+
' return <div>Hello {this.props.name}</div>;',
244271
'}'
245272
].join('\n'),
246273
parser: 'babel-eslint',
247-
options: [{
248-
acceptTranspilerName: true
249-
}],
250274
errors: [{
251275
message: 'Component definition is missing display name'
252276
}]

tests/lib/rules/prop-types.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,49 @@ ruleTester.run('prop-types', rule, {
11671167
errors: [
11681168
{message: '\'firstname\' is missing in props validation for Hello'}
11691169
]
1170+
}, {
1171+
code: [
1172+
'var Hello = function(props) {',
1173+
' return <div>Hello {props.name}</div>;',
1174+
'}'
1175+
].join('\n'),
1176+
parser: 'babel-eslint',
1177+
errors: [{
1178+
message: '\'name\' is missing in props validation'
1179+
}]
1180+
}, {
1181+
code: [
1182+
'function Hello(props) {',
1183+
' return <div>Hello {props.name}</div>;',
1184+
'}'
1185+
].join('\n'),
1186+
parser: 'babel-eslint',
1187+
errors: [{
1188+
message: '\'name\' is missing in props validation for Hello'
1189+
}]
1190+
}, {
1191+
code: [
1192+
'var Hello = (props) => {',
1193+
' return <div>Hello {props.name}</div>;',
1194+
'}'
1195+
].join('\n'),
1196+
parser: 'babel-eslint',
1197+
errors: [{
1198+
message: '\'name\' is missing in props validation'
1199+
}]
1200+
}, {
1201+
code: [
1202+
'class Hello extends React.Component {',
1203+
' render() {',
1204+
' var props = {firstname: \'John\'};',
1205+
' return <div>Hello {props.firstname} {this.props.lastname}</div>;',
1206+
' }',
1207+
'}'
1208+
].join('\n'),
1209+
parser: 'babel-eslint',
1210+
errors: [
1211+
{message: '\'lastname\' is missing in props validation for Hello'}
1212+
]
11701213
}
11711214
]
11721215
});

0 commit comments

Comments
 (0)