source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/boolean-prop-naming.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 12.9 KB
Line 
1/**
2 * @fileoverview Enforces consistent naming for boolean props
3 * @author Ev Haus
4 */
5
6'use strict';
7
8const flatMap = require('array.prototype.flatmap');
9const values = require('object.values');
10
11const Components = require('../util/Components');
12const propsUtil = require('../util/props');
13const docsUrl = require('../util/docsUrl');
14const propWrapperUtil = require('../util/propWrapper');
15const report = require('../util/report');
16const eslintUtil = require('../util/eslint');
17
18const getSourceCode = eslintUtil.getSourceCode;
19const getText = eslintUtil.getText;
20
21// ------------------------------------------------------------------------------
22// Rule Definition
23// ------------------------------------------------------------------------------
24
25const messages = {
26 patternMismatch: 'Prop name `{{propName}}` doesn’t match rule `{{pattern}}`',
27};
28
29/** @type {import('eslint').Rule.RuleModule} */
30module.exports = {
31 meta: {
32 docs: {
33 category: 'Stylistic Issues',
34 description: 'Enforces consistent naming for boolean props',
35 recommended: false,
36 url: docsUrl('boolean-prop-naming'),
37 },
38
39 messages,
40
41 schema: [{
42 additionalProperties: false,
43 properties: {
44 propTypeNames: {
45 items: {
46 type: 'string',
47 },
48 minItems: 1,
49 type: 'array',
50 uniqueItems: true,
51 },
52 rule: {
53 default: '^(is|has)[A-Z]([A-Za-z0-9]?)+',
54 minLength: 1,
55 type: 'string',
56 },
57 message: {
58 minLength: 1,
59 type: 'string',
60 },
61 validateNested: {
62 default: false,
63 type: 'boolean',
64 },
65 },
66 type: 'object',
67 }],
68 },
69
70 create: Components.detect((context, components, utils) => {
71 const config = context.options[0] || {};
72 const rule = config.rule ? new RegExp(config.rule) : null;
73 const propTypeNames = config.propTypeNames || ['bool'];
74
75 // Remembers all Flowtype object definitions
76 const objectTypeAnnotations = new Map();
77
78 /**
79 * Returns the prop key to ensure we handle the following cases:
80 * propTypes: {
81 * full: React.PropTypes.bool,
82 * short: PropTypes.bool,
83 * direct: bool,
84 * required: PropTypes.bool.isRequired
85 * }
86 * @param {Object} node The node we're getting the name of
87 * @returns {string | null}
88 */
89 function getPropKey(node) {
90 // Check for `ExperimentalSpreadProperty` (eslint 3/4) and `SpreadElement` (eslint 5)
91 // so we can skip validation of those fields.
92 // Otherwise it will look for `node.value.property` which doesn't exist and breaks eslint.
93 if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') {
94 return null;
95 }
96 if (node.value && node.value.property) {
97 const name = node.value.property.name;
98 if (name === 'isRequired') {
99 if (node.value.object && node.value.object.property) {
100 return node.value.object.property.name;
101 }
102 return null;
103 }
104 return name;
105 }
106 if (node.value && node.value.type === 'Identifier') {
107 return node.value.name;
108 }
109 return null;
110 }
111
112 /**
113 * Returns the name of the given node (prop)
114 * @param {Object} node The node we're getting the name of
115 * @returns {string}
116 */
117 function getPropName(node) {
118 // Due to this bug https://github.com/babel/babel-eslint/issues/307
119 // we can't get the name of the Flow object key name. So we have
120 // to hack around it for now.
121 if (node.type === 'ObjectTypeProperty') {
122 return getSourceCode(context).getFirstToken(node).value;
123 }
124
125 return node.key.name;
126 }
127
128 /**
129 * Checks if prop is declared in flow way
130 * @param {Object} prop Property object, single prop type declaration
131 * @returns {Boolean}
132 */
133 function flowCheck(prop) {
134 return (
135 prop.type === 'ObjectTypeProperty'
136 && prop.value.type === 'BooleanTypeAnnotation'
137 && rule.test(getPropName(prop)) === false
138 );
139 }
140
141 /**
142 * Checks if prop is declared in regular way
143 * @param {Object} prop Property object, single prop type declaration
144 * @returns {Boolean}
145 */
146 function regularCheck(prop) {
147 const propKey = getPropKey(prop);
148 return (
149 propKey
150 && propTypeNames.indexOf(propKey) >= 0
151 && rule.test(getPropName(prop)) === false
152 );
153 }
154
155 function tsCheck(prop) {
156 if (prop.type !== 'TSPropertySignature') return false;
157 const typeAnnotation = (prop.typeAnnotation || {}).typeAnnotation;
158 return (
159 typeAnnotation
160 && typeAnnotation.type === 'TSBooleanKeyword'
161 && rule.test(getPropName(prop)) === false
162 );
163 }
164
165 /**
166 * Checks if prop is nested
167 * @param {Object} prop Property object, single prop type declaration
168 * @returns {Boolean}
169 */
170 function nestedPropTypes(prop) {
171 return (
172 prop.type === 'Property'
173 && prop.value.type === 'CallExpression'
174 );
175 }
176
177 /**
178 * Runs recursive check on all proptypes
179 * @param {Array} proptypes A list of Property object (for each proptype defined)
180 * @param {Function} addInvalidProp callback to run for each error
181 */
182 function runCheck(proptypes, addInvalidProp) {
183 (proptypes || []).forEach((prop) => {
184 if (config.validateNested && nestedPropTypes(prop)) {
185 runCheck(prop.value.arguments[0].properties, addInvalidProp);
186 return;
187 }
188 if (flowCheck(prop) || regularCheck(prop) || tsCheck(prop)) {
189 addInvalidProp(prop);
190 }
191 });
192 }
193
194 /**
195 * Checks and mark props with invalid naming
196 * @param {Object} node The component node we're testing
197 * @param {Array} proptypes A list of Property object (for each proptype defined)
198 */
199 function validatePropNaming(node, proptypes) {
200 const component = components.get(node) || node;
201 const invalidProps = component.invalidProps || [];
202
203 runCheck(proptypes, (prop) => {
204 invalidProps.push(prop);
205 });
206
207 components.set(node, {
208 invalidProps,
209 });
210 }
211
212 /**
213 * Reports invalid prop naming
214 * @param {Object} component The component to process
215 */
216 function reportInvalidNaming(component) {
217 component.invalidProps.forEach((propNode) => {
218 const propName = getPropName(propNode);
219 report(context, config.message || messages.patternMismatch, !config.message && 'patternMismatch', {
220 node: propNode,
221 data: {
222 component: propName,
223 propName,
224 pattern: config.rule,
225 },
226 });
227 });
228 }
229
230 function checkPropWrapperArguments(node, args) {
231 if (!node || !Array.isArray(args)) {
232 return;
233 }
234 args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
235 }
236
237 function getComponentTypeAnnotation(component) {
238 // If this is a functional component that uses a global type, check it
239 if (
240 (component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
241 && component.node.params
242 && component.node.params.length > 0
243 && component.node.params[0].typeAnnotation
244 ) {
245 return component.node.params[0].typeAnnotation.typeAnnotation;
246 }
247
248 if (
249 !component.node.parent
250 || component.node.parent.type !== 'VariableDeclarator'
251 || !component.node.parent.id
252 || component.node.parent.id.type !== 'Identifier'
253 || !component.node.parent.id.typeAnnotation
254 || !component.node.parent.id.typeAnnotation.typeAnnotation
255 ) {
256 return;
257 }
258
259 const annotationTypeParams = component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters;
260 if (
261 annotationTypeParams && (
262 annotationTypeParams.type === 'TSTypeParameterInstantiation'
263 || annotationTypeParams.type === 'TypeParameterInstantiation'
264 )
265 ) {
266 return annotationTypeParams.params.find(
267 (param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation'
268 );
269 }
270 }
271
272 function findAllTypeAnnotations(identifier, node) {
273 if (node.type === 'TSTypeLiteral' || node.type === 'ObjectTypeAnnotation' || node.type === 'TSInterfaceBody') {
274 const currentNode = [].concat(
275 objectTypeAnnotations.get(identifier.name) || [],
276 node
277 );
278 objectTypeAnnotations.set(identifier.name, currentNode);
279 } else if (
280 node.type === 'TSParenthesizedType'
281 && (
282 node.typeAnnotation.type === 'TSIntersectionType'
283 || node.typeAnnotation.type === 'TSUnionType'
284 )
285 ) {
286 node.typeAnnotation.types.forEach((type) => {
287 findAllTypeAnnotations(identifier, type);
288 });
289 } else if (
290 node.type === 'TSIntersectionType'
291 || node.type === 'TSUnionType'
292 || node.type === 'IntersectionTypeAnnotation'
293 || node.type === 'UnionTypeAnnotation'
294 ) {
295 node.types.forEach((type) => {
296 findAllTypeAnnotations(identifier, type);
297 });
298 }
299 }
300
301 // --------------------------------------------------------------------------
302 // Public
303 // --------------------------------------------------------------------------
304
305 return {
306 'ClassProperty, PropertyDefinition'(node) {
307 if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
308 return;
309 }
310 if (
311 node.value
312 && node.value.type === 'CallExpression'
313 && propWrapperUtil.isPropWrapperFunction(
314 context,
315 getText(context, node.value.callee)
316 )
317 ) {
318 checkPropWrapperArguments(node, node.value.arguments);
319 }
320 if (node.value && node.value.properties) {
321 validatePropNaming(node, node.value.properties);
322 }
323 if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
324 validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
325 }
326 },
327
328 MemberExpression(node) {
329 if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
330 return;
331 }
332 const component = utils.getRelatedComponent(node);
333 if (!component || !node.parent.right) {
334 return;
335 }
336 const right = node.parent.right;
337 if (
338 right.type === 'CallExpression'
339 && propWrapperUtil.isPropWrapperFunction(
340 context,
341 getText(context, right.callee)
342 )
343 ) {
344 checkPropWrapperArguments(component.node, right.arguments);
345 return;
346 }
347 validatePropNaming(component.node, node.parent.right.properties);
348 },
349
350 ObjectExpression(node) {
351 if (!rule) {
352 return;
353 }
354
355 // Search for the proptypes declaration
356 node.properties.forEach((property) => {
357 if (!propsUtil.isPropTypesDeclaration(property)) {
358 return;
359 }
360 validatePropNaming(node, property.value.properties);
361 });
362 },
363
364 TypeAlias(node) {
365 findAllTypeAnnotations(node.id, node.right);
366 },
367
368 TSTypeAliasDeclaration(node) {
369 findAllTypeAnnotations(node.id, node.typeAnnotation);
370 },
371
372 TSInterfaceDeclaration(node) {
373 findAllTypeAnnotations(node.id, node.body);
374 },
375
376 // eslint-disable-next-line object-shorthand
377 'Program:exit'() {
378 if (!rule) {
379 return;
380 }
381
382 values(components.list()).forEach((component) => {
383 const annotation = getComponentTypeAnnotation(component);
384
385 if (annotation) {
386 let propType;
387 if (annotation.type === 'GenericTypeAnnotation') {
388 propType = objectTypeAnnotations.get(annotation.id.name);
389 } else if (annotation.type === 'ObjectTypeAnnotation' || annotation.type === 'TSTypeLiteral') {
390 propType = annotation;
391 } else if (annotation.type === 'TSTypeReference') {
392 propType = objectTypeAnnotations.get(annotation.typeName.name);
393 } else if (annotation.type === 'TSIntersectionType') {
394 propType = flatMap(annotation.types, (type) => (
395 type.type === 'TSTypeReference'
396 ? objectTypeAnnotations.get(type.typeName.name)
397 : type
398 ));
399 }
400
401 if (propType) {
402 [].concat(propType).filter(Boolean).forEach((prop) => {
403 validatePropNaming(
404 component.node,
405 prop.properties || prop.members || prop.body
406 );
407 });
408 }
409 }
410
411 if (component.invalidProps && component.invalidProps.length > 0) {
412 reportInvalidNaming(component);
413 }
414 });
415
416 // Reset cache
417 objectTypeAnnotations.clear();
418 },
419 };
420 }),
421};
Note: See TracBrowser for help on using the repository browser.