UNPKG

9.85 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforces consistent naming for boolean props
3 * @author Ev Haus
4 */
5
6'use strict';
7
8const Components = require('../util/Components');
9const propsUtil = require('../util/props');
10const docsUrl = require('../util/docsUrl');
11const propWrapperUtil = require('../util/propWrapper');
12
13// ------------------------------------------------------------------------------
14// Rule Definition
15// ------------------------------------------------------------------------------
16
17module.exports = {
18 meta: {
19 docs: {
20 category: 'Stylistic Issues',
21 description: 'Enforces consistent naming for boolean props',
22 recommended: false,
23 url: docsUrl('boolean-prop-naming')
24 },
25
26 schema: [{
27 additionalProperties: false,
28 properties: {
29 propTypeNames: {
30 items: {
31 type: 'string'
32 },
33 minItems: 1,
34 type: 'array',
35 uniqueItems: true
36 },
37 rule: {
38 default: '^(is|has)[A-Z]([A-Za-z0-9]?)+',
39 minLength: 1,
40 type: 'string'
41 },
42 message: {
43 minLength: 1,
44 type: 'string'
45 },
46 validateNested: {
47 default: false,
48 type: 'boolean'
49 }
50 },
51 type: 'object'
52 }]
53 },
54
55 create: Components.detect((context, components, utils) => {
56 const config = context.options[0] || {};
57 const rule = config.rule ? new RegExp(config.rule) : null;
58 const propTypeNames = config.propTypeNames || ['bool'];
59
60 // Remembers all Flowtype object definitions
61 const objectTypeAnnotations = new Map();
62
63 /**
64 * Returns the prop key to ensure we handle the following cases:
65 * propTypes: {
66 * full: React.PropTypes.bool,
67 * short: PropTypes.bool,
68 * direct: bool,
69 * required: PropTypes.bool.isRequired
70 * }
71 * @param {Object} node The node we're getting the name of
72 * @returns {string | null}
73 */
74 function getPropKey(node) {
75 // Check for `ExperimentalSpreadProperty` (ESLint 3/4) and `SpreadElement` (ESLint 5)
76 // so we can skip validation of those fields.
77 // Otherwise it will look for `node.value.property` which doesn't exist and breaks ESLint.
78 if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') {
79 return null;
80 }
81 if (node.value.property) {
82 const name = node.value.property.name;
83 if (name === 'isRequired') {
84 if (node.value.object && node.value.object.property) {
85 return node.value.object.property.name;
86 }
87 return null;
88 }
89 return name;
90 }
91 if (node.value.type === 'Identifier') {
92 return node.value.name;
93 }
94 return null;
95 }
96
97 /**
98 * Returns the name of the given node (prop)
99 * @param {Object} node The node we're getting the name of
100 * @returns {string}
101 */
102 function getPropName(node) {
103 // Due to this bug https://github.com/babel/babel-eslint/issues/307
104 // we can't get the name of the Flow object key name. So we have
105 // to hack around it for now.
106 if (node.type === 'ObjectTypeProperty') {
107 return context.getSourceCode().getFirstToken(node).value;
108 }
109
110 return node.key.name;
111 }
112
113 /**
114 * Checks if prop is declared in flow way
115 * @param {Object} prop Property object, single prop type declaration
116 * @returns {Boolean}
117 */
118 function flowCheck(prop) {
119 return (
120 prop.type === 'ObjectTypeProperty'
121 && prop.value.type === 'BooleanTypeAnnotation'
122 && rule.test(getPropName(prop)) === false
123 );
124 }
125
126 /**
127 * Checks if prop is declared in regular way
128 * @param {Object} prop Property object, single prop type declaration
129 * @returns {Boolean}
130 */
131 function regularCheck(prop) {
132 const propKey = getPropKey(prop);
133 return (
134 propKey
135 && propTypeNames.indexOf(propKey) >= 0
136 && rule.test(getPropName(prop)) === false
137 );
138 }
139
140 /**
141 * Checks if prop is nested
142 * @param {Object} prop Property object, single prop type declaration
143 * @returns {Boolean}
144 */
145 function nestedPropTypes(prop) {
146 return (
147 prop.type === 'Property'
148 && prop.value.type === 'CallExpression'
149 );
150 }
151
152 /**
153 * Runs recursive check on all proptypes
154 * @param {Array} proptypes A list of Property object (for each proptype defined)
155 * @param {Function} addInvalidProp callback to run for each error
156 */
157 function runCheck(proptypes, addInvalidProp) {
158 proptypes = proptypes || [];
159
160 proptypes.forEach((prop) => {
161 if (config.validateNested && nestedPropTypes(prop)) {
162 runCheck(prop.value.arguments[0].properties, addInvalidProp);
163 return;
164 }
165 if (flowCheck(prop) || regularCheck(prop)) {
166 addInvalidProp(prop);
167 }
168 });
169 }
170
171 /**
172 * Checks and mark props with invalid naming
173 * @param {Object} node The component node we're testing
174 * @param {Array} proptypes A list of Property object (for each proptype defined)
175 */
176 function validatePropNaming(node, proptypes) {
177 const component = components.get(node) || node;
178 const invalidProps = component.invalidProps || [];
179
180 runCheck(proptypes, (prop) => {
181 invalidProps.push(prop);
182 });
183
184 components.set(node, {
185 invalidProps
186 });
187 }
188
189 /**
190 * Reports invalid prop naming
191 * @param {Object} component The component to process
192 */
193 function reportInvalidNaming(component) {
194 component.invalidProps.forEach((propNode) => {
195 const propName = getPropName(propNode);
196 context.report({
197 node: propNode,
198 message: config.message || 'Prop name ({{ propName }}) doesn\'t match rule ({{ pattern }})',
199 data: {
200 component: propName,
201 propName,
202 pattern: config.rule
203 }
204 });
205 });
206 }
207
208 function checkPropWrapperArguments(node, args) {
209 if (!node || !Array.isArray(args)) {
210 return;
211 }
212 args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
213 }
214
215 // --------------------------------------------------------------------------
216 // Public
217 // --------------------------------------------------------------------------
218
219 return {
220 ClassProperty(node) {
221 if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
222 return;
223 }
224 if (
225 node.value
226 && node.value.type === 'CallExpression'
227 && propWrapperUtil.isPropWrapperFunction(
228 context,
229 context.getSourceCode().getText(node.value.callee)
230 )
231 ) {
232 checkPropWrapperArguments(node, node.value.arguments);
233 }
234 if (node.value && node.value.properties) {
235 validatePropNaming(node, node.value.properties);
236 }
237 if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
238 validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
239 }
240 },
241
242 MemberExpression(node) {
243 if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
244 return;
245 }
246 const component = utils.getRelatedComponent(node);
247 if (!component || !node.parent.right) {
248 return;
249 }
250 const right = node.parent.right;
251 if (
252 right.type === 'CallExpression'
253 && propWrapperUtil.isPropWrapperFunction(
254 context,
255 context.getSourceCode().getText(right.callee)
256 )
257 ) {
258 checkPropWrapperArguments(component.node, right.arguments);
259 return;
260 }
261 validatePropNaming(component.node, node.parent.right.properties);
262 },
263
264 ObjectExpression(node) {
265 if (!rule) {
266 return;
267 }
268
269 // Search for the proptypes declaration
270 node.properties.forEach((property) => {
271 if (!propsUtil.isPropTypesDeclaration(property)) {
272 return;
273 }
274 validatePropNaming(node, property.value.properties);
275 });
276 },
277
278 TypeAlias(node) {
279 // Cache all ObjectType annotations, we will check them at the end
280 if (node.right.type === 'ObjectTypeAnnotation') {
281 objectTypeAnnotations.set(node.id.name, node.right);
282 }
283 },
284
285 // eslint-disable-next-line object-shorthand
286 'Program:exit'() {
287 if (!rule) {
288 return;
289 }
290
291 const list = components.list();
292 Object.keys(list).forEach((component) => {
293 // If this is a functional component that uses a global type, check it
294 if (
295 list[component].node.type === 'FunctionDeclaration'
296 && list[component].node.params
297 && list[component].node.params.length
298 && list[component].node.params[0].typeAnnotation
299 ) {
300 const typeNode = list[component].node.params[0].typeAnnotation;
301 const annotation = typeNode.typeAnnotation;
302
303 let propType;
304 if (annotation.type === 'GenericTypeAnnotation') {
305 propType = objectTypeAnnotations.get(annotation.id.name);
306 } else if (annotation.type === 'ObjectTypeAnnotation') {
307 propType = annotation;
308 }
309 if (propType) {
310 validatePropNaming(list[component].node, propType.properties);
311 }
312 }
313
314 if (list[component].invalidProps && list[component].invalidProps.length > 0) {
315 reportInvalidNaming(list[component]);
316 }
317 });
318
319 // Reset cache
320 objectTypeAnnotations.clear();
321 }
322 };
323 })
324};