boolean-prop-naming.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. // Predefine message for use in context.report conditional.
  14. // messageId will still be usable in tests.
  15. const PATTERN_MISMATCH_MSG = 'Prop name ({{propName}}) doesn\'t match rule ({{pattern}})';
  16. module.exports = {
  17. meta: {
  18. docs: {
  19. category: 'Stylistic Issues',
  20. description: 'Enforces consistent naming for boolean props',
  21. recommended: false,
  22. url: docsUrl('boolean-prop-naming')
  23. },
  24. messages: {
  25. patternMismatch: PATTERN_MISMATCH_MSG
  26. },
  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 = proptypes || [];
  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) || tsCheck(prop)) {
  166. addInvalidProp(prop);
  167. }
  168. });
  169. }
  170. /**
  171. * Checks and mark props with invalid naming
  172. * @param {Object} node The component node we're testing
  173. * @param {Array} proptypes A list of Property object (for each proptype defined)
  174. */
  175. function validatePropNaming(node, proptypes) {
  176. const component = components.get(node) || node;
  177. const invalidProps = component.invalidProps || [];
  178. runCheck(proptypes, (prop) => {
  179. invalidProps.push(prop);
  180. });
  181. components.set(node, {
  182. invalidProps
  183. });
  184. }
  185. /**
  186. * Reports invalid prop naming
  187. * @param {Object} component The component to process
  188. */
  189. function reportInvalidNaming(component) {
  190. component.invalidProps.forEach((propNode) => {
  191. const propName = getPropName(propNode);
  192. context.report(Object.assign({
  193. node: propNode,
  194. data: {
  195. component: propName,
  196. propName,
  197. pattern: config.rule
  198. }
  199. }, config.message ? {message: config.message} : {messageId: 'patternMismatch'}));
  200. });
  201. }
  202. function checkPropWrapperArguments(node, args) {
  203. if (!node || !Array.isArray(args)) {
  204. return;
  205. }
  206. args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
  207. }
  208. // --------------------------------------------------------------------------
  209. // Public
  210. // --------------------------------------------------------------------------
  211. return {
  212. ClassProperty(node) {
  213. if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
  214. return;
  215. }
  216. if (
  217. node.value
  218. && node.value.type === 'CallExpression'
  219. && propWrapperUtil.isPropWrapperFunction(
  220. context,
  221. context.getSourceCode().getText(node.value.callee)
  222. )
  223. ) {
  224. checkPropWrapperArguments(node, node.value.arguments);
  225. }
  226. if (node.value && node.value.properties) {
  227. validatePropNaming(node, node.value.properties);
  228. }
  229. if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
  230. validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
  231. }
  232. },
  233. MemberExpression(node) {
  234. if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
  235. return;
  236. }
  237. const component = utils.getRelatedComponent(node);
  238. if (!component || !node.parent.right) {
  239. return;
  240. }
  241. const right = node.parent.right;
  242. if (
  243. right.type === 'CallExpression'
  244. && propWrapperUtil.isPropWrapperFunction(
  245. context,
  246. context.getSourceCode().getText(right.callee)
  247. )
  248. ) {
  249. checkPropWrapperArguments(component.node, right.arguments);
  250. return;
  251. }
  252. validatePropNaming(component.node, node.parent.right.properties);
  253. },
  254. ObjectExpression(node) {
  255. if (!rule) {
  256. return;
  257. }
  258. // Search for the proptypes declaration
  259. node.properties.forEach((property) => {
  260. if (!propsUtil.isPropTypesDeclaration(property)) {
  261. return;
  262. }
  263. validatePropNaming(node, property.value.properties);
  264. });
  265. },
  266. TypeAlias(node) {
  267. // Cache all ObjectType annotations, we will check them at the end
  268. if (node.right.type === 'ObjectTypeAnnotation') {
  269. objectTypeAnnotations.set(node.id.name, node.right);
  270. }
  271. },
  272. TSTypeAliasDeclaration(node) {
  273. if (node.typeAnnotation.type === 'TSTypeLiteral') {
  274. objectTypeAnnotations.set(node.id.name, node.typeAnnotation);
  275. }
  276. },
  277. // eslint-disable-next-line object-shorthand
  278. 'Program:exit'() {
  279. if (!rule) {
  280. return;
  281. }
  282. const list = components.list();
  283. Object.keys(list).forEach((component) => {
  284. // If this is a functional component that uses a global type, check it
  285. if (
  286. (
  287. list[component].node.type === 'FunctionDeclaration'
  288. || list[component].node.type === 'ArrowFunctionExpression'
  289. )
  290. && list[component].node.params
  291. && list[component].node.params.length
  292. && list[component].node.params[0].typeAnnotation
  293. ) {
  294. const typeNode = list[component].node.params[0].typeAnnotation;
  295. const annotation = typeNode.typeAnnotation;
  296. let propType;
  297. if (annotation.type === 'GenericTypeAnnotation') {
  298. propType = objectTypeAnnotations.get(annotation.id.name);
  299. } else if (annotation.type === 'ObjectTypeAnnotation') {
  300. propType = annotation;
  301. } else if (annotation.type === 'TSTypeReference') {
  302. propType = objectTypeAnnotations.get(annotation.typeName.name);
  303. }
  304. if (propType) {
  305. validatePropNaming(
  306. list[component].node,
  307. propType.properties || propType.members
  308. );
  309. }
  310. }
  311. if (list[component].invalidProps && list[component].invalidProps.length > 0) {
  312. reportInvalidNaming(list[component]);
  313. }
  314. });
  315. // Reset cache
  316. objectTypeAnnotations.clear();
  317. }
  318. };
  319. })
  320. };