boolean-prop-naming.js 12 KB

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