Skip to content

Commit 15f0925

Browse files
committed
Fix display-name for stateless components (fixes #256)
1 parent b094b99 commit 15f0925

File tree

4 files changed

+235
-28
lines changed

4 files changed

+235
-28
lines changed

lib/rules/display-name.js

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ module.exports = function(context) {
7575
* @returns {Boolean} True ifcomponent have a name, false if not.
7676
*/
7777
function hasTranspilerName(node) {
78-
var namedAssignment = (
78+
var namedObjectAssignment = (
7979
node.type === 'ObjectExpression' &&
8080
node.parent &&
8181
node.parent.parent &&
@@ -85,7 +85,7 @@ module.exports = function(context) {
8585
node.parent.parent.left.property.name !== 'exports'
8686
)
8787
);
88-
var namedDeclaration = (
88+
var namedObjectDeclaration = (
8989
node.type === 'ObjectExpression' &&
9090
node.parent &&
9191
node.parent.parent &&
@@ -96,7 +96,23 @@ module.exports = function(context) {
9696
node.id && node.id.name
9797
);
9898

99-
if (namedAssignment || namedDeclaration || namedClass) {
99+
var namedFunctionDeclaration = (
100+
(node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') &&
101+
node.id &&
102+
node.id.name
103+
);
104+
105+
var namedFunctionExpression = (
106+
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
107+
node.parent &&
108+
(node.parent.type === 'VariableDeclarator' || node.parent.method === true)
109+
);
110+
111+
if (
112+
namedObjectAssignment || namedObjectDeclaration ||
113+
namedClass ||
114+
namedFunctionDeclaration || namedFunctionExpression
115+
) {
100116
return true;
101117
}
102118
return false;
@@ -119,13 +135,37 @@ module.exports = function(context) {
119135
if (!isDisplayNameDeclaration(node.property)) {
120136
return;
121137
}
122-
var component = componentList.getByName(node.object.name);
138+
var component = componentList.getByName(context.getSource(node.object));
123139
if (!component) {
124140
return;
125141
}
126142
markDisplayNameAsDeclared(component.node);
127143
},
128144

145+
FunctionExpression: function(node) {
146+
componentList.set(context, node);
147+
if (!acceptTranspilerName || !hasTranspilerName(node)) {
148+
return;
149+
}
150+
markDisplayNameAsDeclared(node);
151+
},
152+
153+
FunctionDeclaration: function(node) {
154+
componentList.set(context, node);
155+
if (!acceptTranspilerName || !hasTranspilerName(node)) {
156+
return;
157+
}
158+
markDisplayNameAsDeclared(node);
159+
},
160+
161+
ArrowFunctionExpression: function(node) {
162+
componentList.set(context, node);
163+
if (!acceptTranspilerName || !hasTranspilerName(node)) {
164+
return;
165+
}
166+
markDisplayNameAsDeclared(node);
167+
},
168+
129169
MethodDefinition: function(node) {
130170
if (!isDisplayNameDeclaration(node.key)) {
131171
return;

lib/util/component.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,17 @@ function isStatelessFunctionComponent(context, node) {
7272
* @returns {Object} The component identifiers.
7373
*/
7474
function getIdentifiers(node) {
75-
var name = node.id && node.id.name || DEFAULT_COMPONENT_NAME;
75+
var name = [];
76+
var loopNode = node;
77+
var namePart = [];
78+
while (loopNode) {
79+
namePart = (loopNode.id && loopNode.id.name) || (loopNode.key && loopNode.key.name);
80+
if (namePart) {
81+
name.unshift(namePart);
82+
}
83+
loopNode = loopNode.parent;
84+
}
85+
name = name.join('.') || DEFAULT_COMPONENT_NAME;
7686
var id = name + ':' + node.loc.start.line + ':' + node.loc.start.column;
7787

7888
return {
@@ -98,7 +108,7 @@ function getNode(context, node, list) {
98108
}
99109
// Node is already in the component list
100110
var identifiers = getIdentifiers(ancestors[i]);
101-
if (list && list[identifiers.id]) {
111+
if (list && list[identifiers.id] && list[identifiers.id].isComponent) {
102112
return ancestors[i];
103113
}
104114
}
@@ -136,7 +146,7 @@ List.prototype.getByNode = function(context, node) {
136146
}
137147
var identifiers = getIdentifiers(componentNode);
138148

139-
return this._list[identifiers.id] || null;
149+
return this._list[identifiers.id] && this._list[identifiers.id].isComponent ? this._list[identifiers.id] : null;
140150
};
141151

142152
/**
@@ -146,7 +156,11 @@ List.prototype.getByNode = function(context, node) {
146156
*/
147157
List.prototype.getByName = function(name) {
148158
for (var component in this._list) {
149-
if (this._list.hasOwnProperty(component) && this._list[component].name === name) {
159+
if (
160+
this._list.hasOwnProperty(component) &&
161+
this._list[component].name === name &&
162+
this._list[component].isComponent
163+
) {
150164
return this._list[component];
151165
}
152166
}
@@ -158,7 +172,14 @@ List.prototype.getByName = function(name) {
158172
* @returns {Object} The component list.
159173
*/
160174
List.prototype.getList = function() {
161-
return this._list;
175+
var components = {};
176+
for (var component in this._list) {
177+
if (!this._list.hasOwnProperty(component) || this._list[component].isComponent !== true) {
178+
continue;
179+
}
180+
components[component] = this._list[component];
181+
}
182+
return components;
162183
};
163184

164185
/**
@@ -170,15 +191,18 @@ List.prototype.getList = function() {
170191
*/
171192
List.prototype.set = function(context, node, customProperties) {
172193
var componentNode = getNode(context, node, this._list);
173-
if (!componentNode) {
174-
return null;
194+
var isComponent = false;
195+
if (componentNode) {
196+
node = componentNode;
197+
isComponent = true;
175198
}
176199

177-
var identifiers = getIdentifiers(componentNode);
200+
var identifiers = getIdentifiers(node);
178201

179202
var component = util._extend({
180203
name: identifiers.name,
181-
node: componentNode
204+
node: node,
205+
isComponent: isComponent
182206
}, customProperties || {});
183207

184208
if (!this._list[identifiers.id]) {

tests/lib/rules/display-name.js

Lines changed: 152 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,124 @@ ruleTester.run('display-name', rule, {
178178
'}'
179179
].join('\n'),
180180
parser: 'babel-eslint'
181+
}, {
182+
code: [
183+
'var Hello = function() {',
184+
' return <div>Hello {this.props.name}</div>;',
185+
'}'
186+
].join('\n'),
187+
parser: 'babel-eslint',
188+
options: [{
189+
acceptTranspilerName: true
190+
}]
191+
}, {
192+
code: [
193+
'function Hello() {',
194+
' return <div>Hello {this.props.name}</div>;',
195+
'}'
196+
].join('\n'),
197+
parser: 'babel-eslint',
198+
options: [{
199+
acceptTranspilerName: true
200+
}]
201+
}, {
202+
code: [
203+
'var Hello = () => {',
204+
' return <div>Hello {this.props.name}</div>;',
205+
'}'
206+
].join('\n'),
207+
parser: 'babel-eslint',
208+
options: [{
209+
acceptTranspilerName: true
210+
}]
211+
}, {
212+
code: [
213+
'module.exports = function Hello() {',
214+
' return <div>Hello {this.props.name}</div>;',
215+
'}'
216+
].join('\n'),
217+
parser: 'babel-eslint',
218+
options: [{
219+
acceptTranspilerName: true
220+
}]
221+
}, {
222+
code: [
223+
'function Hello() {',
224+
' return <div>Hello {this.props.name}</div>;',
225+
'}',
226+
'Hello.displayName = \'Hello\';'
227+
].join('\n'),
228+
parser: 'babel-eslint'
229+
}, {
230+
code: [
231+
'var Hello = () => {',
232+
' return <div>Hello {this.props.name}</div>;',
233+
'}',
234+
'Hello.displayName = \'Hello\';'
235+
].join('\n'),
236+
parser: 'babel-eslint'
237+
}, {
238+
code: [
239+
'var Hello = function() {',
240+
' return <div>Hello {this.props.name}</div>;',
241+
'}',
242+
'Hello.displayName = \'Hello\';'
243+
].join('\n'),
244+
parser: 'babel-eslint'
245+
}, {
246+
code: [
247+
'var Mixins = {',
248+
' Greetings: {',
249+
' Hello: function() {',
250+
' return <div>Hello {this.props.name}</div>;',
251+
' }',
252+
' }',
253+
'}',
254+
'Mixins.Greetings.Hello.displayName = \'Hello\';'
255+
].join('\n'),
256+
parser: 'babel-eslint'
257+
}, {
258+
code: [
259+
'var Hello = React.createClass({',
260+
' render: function() {',
261+
' return <div>{this._renderHello()}</div>;',
262+
' },',
263+
' _renderHello: function() {',
264+
' return <span>Hello {this.props.name}</span>;',
265+
' }',
266+
'});'
267+
].join('\n'),
268+
parser: 'babel-eslint',
269+
options: [{
270+
acceptTranspilerName: true
271+
}]
272+
}, {
273+
code: [
274+
'var Hello = React.createClass({',
275+
' displayName: \'Hello\',',
276+
' render: function() {',
277+
' return <div>{this._renderHello()}</div>;',
278+
' },',
279+
' _renderHello: function() {',
280+
' return <span>Hello {this.props.name}</span>;',
281+
' }',
282+
'});'
283+
].join('\n'),
284+
parser: 'babel-eslint'
285+
}, {
286+
code: [
287+
'const Mixin = {',
288+
' Button() {',
289+
' return (',
290+
' <button />',
291+
' );',
292+
' }',
293+
'};'
294+
].join('\n'),
295+
parser: 'babel-eslint',
296+
options: [{
297+
acceptTranspilerName: true
298+
}]
181299
}],
182300

183301
invalid: [{
@@ -192,7 +310,7 @@ ruleTester.run('display-name', rule, {
192310
jsx: false
193311
},
194312
errors: [{
195-
message: 'Component definition is missing display name'
313+
message: 'Hello component definition is missing display name'
196314
}]
197315
}, {
198316
code: [
@@ -206,7 +324,7 @@ ruleTester.run('display-name', rule, {
206324
jsx: true
207325
},
208326
errors: [{
209-
message: 'Component definition is missing display name'
327+
message: 'Hello component definition is missing display name'
210328
}]
211329
}, {
212330
code: [
@@ -242,37 +360,62 @@ ruleTester.run('display-name', rule, {
242360
jsx: true
243361
},
244362
errors: [{
245-
message: 'Component definition is missing display name'
363+
message: 'HelloComponent component definition is missing display name'
246364
}]
247365
}, {
248366
code: [
249-
'var Hello = function() {',
367+
'module.exports = () => {',
250368
' return <div>Hello {this.props.name}</div>;',
251369
'}'
252370
].join('\n'),
253371
parser: 'babel-eslint',
372+
options: [{
373+
acceptTranspilerName: true
374+
}],
254375
errors: [{
255376
message: 'Component definition is missing display name'
256377
}]
257378
}, {
258379
code: [
259-
'function Hello() {',
380+
'module.exports = function() {',
260381
' return <div>Hello {this.props.name}</div>;',
261382
'}'
262383
].join('\n'),
263384
parser: 'babel-eslint',
385+
options: [{
386+
acceptTranspilerName: true
387+
}],
388+
errors: [{
389+
message: 'Component definition is missing display name'
390+
}]
391+
}, {
392+
code: [
393+
'var Hello = React.createClass({',
394+
' _renderHello: function() {',
395+
' return <span>Hello {this.props.name}</span>;',
396+
' },',
397+
' render: function() {',
398+
' return <div>{this._renderHello()}</div>;',
399+
' }',
400+
'});'
401+
].join('\n'),
402+
parser: 'babel-eslint',
264403
errors: [{
265404
message: 'Hello component definition is missing display name'
266405
}]
267406
}, {
268407
code: [
269-
'var Hello = () => {',
270-
' return <div>Hello {this.props.name}</div>;',
271-
'}'
408+
'const Mixin = {',
409+
' Button() {',
410+
' return (',
411+
' <button />',
412+
' );',
413+
' }',
414+
'};'
272415
].join('\n'),
273416
parser: 'babel-eslint',
274417
errors: [{
275-
message: 'Component definition is missing display name'
418+
message: 'Mixin.Button component definition is missing display name'
276419
}]
277420
}
278421
]});

0 commit comments

Comments
 (0)