10
10
11
11
module . exports = function ( context ) {
12
12
13
- var hasDisplayName = false ;
13
+ var components = { } ;
14
14
15
+ var MISSING_MESSAGE = 'Component definition is missing display name' ;
16
+ var MISSING_MESSAGE_NAMED_COMP = '{{component}} component definition is missing display name' ;
17
+
18
+ var defaultClassName = 'eslintReactComponent' ;
19
+
20
+ /**
21
+ * Get the component id from an ASTNode
22
+ * @param {ASTNode } node The AST node being checked.
23
+ * @returns {String } The component id.
24
+ */
25
+ function getComponentId ( node ) {
26
+ if (
27
+ node . type === 'MemberExpression' &&
28
+ node . property && node . property . name === 'displayName' &&
29
+ node . object && components [ node . object . name ]
30
+ ) {
31
+ return node . object . name ;
32
+ }
33
+
34
+ var scope = context . getScope ( ) ;
35
+ while ( scope && scope . type !== 'class' ) {
36
+ scope = scope . upper ;
37
+ }
38
+
39
+ if ( scope ) {
40
+ return scope . block . id . name ;
41
+ }
42
+
43
+ return defaultClassName ;
44
+ }
45
+
46
+ /**
47
+ * Get the component from an ASTNode
48
+ * @param {ASTNode } node The AST node being checked.
49
+ * @returns {Object } The component object.
50
+ */
51
+ function getComponent ( node ) {
52
+ var id = getComponentId ( node ) ;
53
+ if ( ! components [ id ] ) {
54
+ components [ id ] = {
55
+ name : id ,
56
+ node : node ,
57
+ hasDisplayName : false
58
+ } ;
59
+ }
60
+ return components [ id ] ;
61
+ }
62
+
63
+ /**
64
+ * Detect if we are in a React component by checking the render method
65
+ * @param {ASTNode } node The AST node being checked.
66
+ */
67
+ function detectReactComponent ( node ) {
68
+ var scope = context . getScope ( ) ;
69
+ if (
70
+ ( node . argument . type === 'Literal' && ( node . argument . value !== null && node . argument . value !== false ) ) &&
71
+ ( node . argument . type !== 'JSXElement' ) &&
72
+ ( scope . block . parent . key . name === 'render' )
73
+ ) {
74
+ return ;
75
+ }
76
+ var component = getComponent ( node ) ;
77
+ component . isComponentDefinition = true ;
78
+ }
79
+
80
+ /**
81
+ * Checks if we are inside a component definition
82
+ * @param {ASTNode } node The AST node being checked.
83
+ * @returns {Boolean } True if we are inside a component definition, false if not.
84
+ */
15
85
function isComponentDefinition ( node ) {
16
- return (
86
+ var isES5Component = Boolean (
87
+ node . parent &&
88
+ node . parent . callee &&
89
+ node . parent . callee . object &&
90
+ node . parent . callee . property &&
91
+ node . parent . callee . object . name === 'React' &&
92
+ node . parent . callee . property . name === 'createClass'
93
+ ) ;
94
+ var isES6Component = getComponent ( node ) . isComponentDefinition ;
95
+ return isES5Component || isES6Component ;
96
+ }
97
+
98
+ /**
99
+ * Checks if we are declaring a display name
100
+ * @param {ASTNode } node The AST node being checked.
101
+ * @returns {Boolean } True if we are declaring a display name, false if not.
102
+ */
103
+ function isDisplayNameDeclaration ( node ) {
104
+ return Boolean (
17
105
node &&
18
- node . callee &&
19
- node . callee . object &&
20
- node . callee . property &&
21
- node . callee . object . name === 'React' &&
22
- node . callee . property . name === 'createClass'
106
+ node . name === 'displayName'
107
+ ) ;
108
+ }
109
+
110
+ /**
111
+ * Mark a prop type as declared
112
+ * @param {ASTNode } node The AST node being checked.
113
+ */
114
+ function markDisplayNameAsDeclared ( node ) {
115
+ var component = getComponent ( node ) ;
116
+ component . hasDisplayName = true ;
117
+ }
118
+
119
+ /**
120
+ * Reports missing display name for a given component
121
+ * @param {String } id The id of the component to process
122
+ */
123
+ function reportMissingDisplayName ( id ) {
124
+ if ( ! components [ id ] || components [ id ] . hasDisplayName === true ) {
125
+ return ;
126
+ }
127
+ context . report (
128
+ components [ id ] . node ,
129
+ id === defaultClassName ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP , {
130
+ component : id
131
+ }
23
132
) ;
24
133
}
25
134
@@ -29,32 +138,50 @@ module.exports = function(context) {
29
138
30
139
return {
31
140
32
- ObjectExpression : function ( node ) {
141
+ MemberExpression : function ( node ) {
142
+ if ( ! isDisplayNameDeclaration ( node . property ) ) {
143
+ return ;
144
+ }
145
+ markDisplayNameAsDeclared ( node ) ;
146
+ } ,
33
147
34
- if ( ! isComponentDefinition ( node . parent ) ) {
148
+ ObjectExpression : function ( node ) {
149
+ if ( ! isComponentDefinition ( node ) ) {
35
150
return ;
36
151
}
37
152
153
+ // Search for the displayName declaration
38
154
node . properties . forEach ( function ( property ) {
39
- var keyName = property . key . name || property . key . value ;
40
- if ( keyName === 'displayName' ) {
41
- hasDisplayName = true ;
155
+ if ( ! isDisplayNameDeclaration ( property . key ) ) {
156
+ return ;
42
157
}
158
+ markDisplayNameAsDeclared ( node ) ;
43
159
} ) ;
44
160
} ,
45
161
46
162
'ObjectExpression:exit' : function ( node ) {
47
-
48
- if ( ! isComponentDefinition ( node . parent ) ) {
163
+ if ( ! isComponentDefinition ( node ) ) {
49
164
return ;
50
165
}
51
166
52
- if ( ! hasDisplayName ) {
53
- context . report ( node , 'Component definition is missing display name' ) ;
167
+ // Report missing display name for all ES5 classes
168
+ reportMissingDisplayName ( defaultClassName ) ;
169
+
170
+ // Reset the ES5 default object
171
+ components [ defaultClassName ] = null ;
172
+ } ,
173
+
174
+ 'Program:exit' : function ( ) {
175
+ // Report missing display name for all ES6 classes
176
+ for ( var component in components ) {
177
+ if ( ! components . hasOwnProperty ( component ) ) {
178
+ continue ;
179
+ }
180
+ reportMissingDisplayName ( component ) ;
54
181
}
182
+ } ,
55
183
56
- hasDisplayName = false ;
57
- }
184
+ ReturnStatement : detectReactComponent
58
185
} ;
59
186
60
187
} ;
0 commit comments