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