no-typos.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /**
  2. * @fileoverview Prevent common casing typos
  3. */
  4. 'use strict';
  5. const PROP_TYPES = Object.keys(require('prop-types'));
  6. const Components = require('../util/Components');
  7. const docsUrl = require('../util/docsUrl');
  8. const astUtil = require('../util/ast');
  9. const componentUtil = require('../util/componentUtil');
  10. const report = require('../util/report');
  11. const lifecycleMethods = require('../util/lifecycleMethods');
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps'];
  16. const messages = {
  17. typoPropTypeChain: 'Typo in prop type chain qualifier: {{name}}',
  18. typoPropType: 'Typo in declared prop type: {{name}}',
  19. typoStaticClassProp: 'Typo in static class property declaration',
  20. typoPropDeclaration: 'Typo in property declaration',
  21. typoLifecycleMethod: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}',
  22. staticLifecycleMethod: 'Lifecycle method should be static: {{method}}',
  23. noPropTypesBinding: '`\'prop-types\'` imported without a local `PropTypes` binding.',
  24. noReactBinding: '`\'react\'` imported without a local `React` binding.',
  25. };
  26. /** @type {import('eslint').Rule.RuleModule} */
  27. module.exports = {
  28. meta: {
  29. docs: {
  30. description: 'Disallow common typos',
  31. category: 'Stylistic Issues',
  32. recommended: false,
  33. url: docsUrl('no-typos'),
  34. },
  35. messages,
  36. schema: [],
  37. },
  38. create: Components.detect((context, components, utils) => {
  39. let propTypesPackageName = null;
  40. let reactPackageName = null;
  41. function checkValidPropTypeQualifier(node) {
  42. if (node.name !== 'isRequired') {
  43. report(context, messages.typoPropTypeChain, 'typoPropTypeChain', {
  44. node,
  45. data: { name: node.name },
  46. });
  47. }
  48. }
  49. function checkValidPropType(node) {
  50. if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) {
  51. report(context, messages.typoPropType, 'typoPropType', {
  52. node,
  53. data: { name: node.name },
  54. });
  55. }
  56. }
  57. function isPropTypesPackage(node) {
  58. return (
  59. node.type === 'Identifier'
  60. && node.name === propTypesPackageName
  61. ) || (
  62. node.type === 'MemberExpression'
  63. && node.property.name === 'PropTypes'
  64. && node.object.name === reactPackageName
  65. );
  66. }
  67. /* eslint-disable no-use-before-define */
  68. function checkValidCallExpression(node) {
  69. const callee = node.callee;
  70. if (callee.type === 'MemberExpression' && callee.property.name === 'shape') {
  71. checkValidPropObject(node.arguments[0]);
  72. } else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') {
  73. const args = node.arguments[0];
  74. if (args && args.type === 'ArrayExpression') {
  75. args.elements.forEach((el) => {
  76. checkValidProp(el);
  77. });
  78. }
  79. }
  80. }
  81. function checkValidProp(node) {
  82. if ((!propTypesPackageName && !reactPackageName) || !node) {
  83. return;
  84. }
  85. if (node.type === 'MemberExpression') {
  86. if (
  87. node.object.type === 'MemberExpression'
  88. && isPropTypesPackage(node.object.object)
  89. ) { // PropTypes.myProp.isRequired
  90. checkValidPropType(node.object.property);
  91. checkValidPropTypeQualifier(node.property);
  92. } else if (
  93. isPropTypesPackage(node.object)
  94. && node.property.name !== 'isRequired'
  95. ) { // PropTypes.myProp
  96. checkValidPropType(node.property);
  97. } else if (astUtil.isCallExpression(node.object)) {
  98. checkValidPropTypeQualifier(node.property);
  99. checkValidCallExpression(node.object);
  100. }
  101. } else if (astUtil.isCallExpression(node)) {
  102. checkValidCallExpression(node);
  103. }
  104. }
  105. /* eslint-enable no-use-before-define */
  106. function checkValidPropObject(node) {
  107. if (node && node.type === 'ObjectExpression') {
  108. node.properties.forEach((prop) => checkValidProp(prop.value));
  109. }
  110. }
  111. function reportErrorIfPropertyCasingTypo(propertyValue, propertyKey, isClassProperty) {
  112. const propertyName = propertyKey.name;
  113. if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
  114. checkValidPropObject(propertyValue);
  115. }
  116. STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => {
  117. if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
  118. const messageId = isClassProperty
  119. ? 'typoStaticClassProp'
  120. : 'typoPropDeclaration';
  121. report(context, messages[messageId], messageId, {
  122. node: propertyKey,
  123. });
  124. }
  125. });
  126. }
  127. function reportErrorIfLifecycleMethodCasingTypo(node) {
  128. const key = node.key;
  129. let nodeKeyName = key.name;
  130. if (key.type === 'Literal') {
  131. nodeKeyName = key.value;
  132. }
  133. if (key.type === 'PrivateName' || (node.computed && typeof nodeKeyName !== 'string')) {
  134. return;
  135. }
  136. lifecycleMethods.static.forEach((method) => {
  137. if (!node.static && nodeKeyName && nodeKeyName.toLowerCase() === method.toLowerCase()) {
  138. report(context, messages.staticLifecycleMethod, 'staticLifecycleMethod', {
  139. node,
  140. data: {
  141. method: nodeKeyName,
  142. },
  143. });
  144. }
  145. });
  146. lifecycleMethods.instance.concat(lifecycleMethods.static).forEach((method) => {
  147. if (nodeKeyName && method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) {
  148. report(context, messages.typoLifecycleMethod, 'typoLifecycleMethod', {
  149. node,
  150. data: { actual: nodeKeyName, expected: method },
  151. });
  152. }
  153. });
  154. }
  155. return {
  156. ImportDeclaration(node) {
  157. if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
  158. if (node.specifiers.length > 0) {
  159. propTypesPackageName = node.specifiers[0].local.name;
  160. } else {
  161. report(context, messages.noPropTypesBinding, 'noPropTypesBinding', {
  162. node,
  163. });
  164. }
  165. } else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
  166. if (node.specifiers.length > 0) {
  167. reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
  168. } else {
  169. report(context, messages.noReactBinding, 'noReactBinding', {
  170. node,
  171. });
  172. }
  173. if (node.specifiers.length >= 1) {
  174. const propTypesSpecifier = node.specifiers.find((specifier) => (
  175. specifier.imported
  176. && specifier.imported.name === 'PropTypes'
  177. ));
  178. if (propTypesSpecifier) {
  179. propTypesPackageName = propTypesSpecifier.local.name;
  180. }
  181. }
  182. }
  183. },
  184. 'ClassProperty, PropertyDefinition'(node) {
  185. if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) {
  186. return;
  187. }
  188. reportErrorIfPropertyCasingTypo(node.value, node.key, true);
  189. },
  190. MemberExpression(node) {
  191. const propertyName = node.property.name;
  192. if (
  193. !propertyName
  194. || STATIC_CLASS_PROPERTIES.map((prop) => prop.toLocaleLowerCase()).indexOf(propertyName.toLowerCase()) === -1
  195. ) {
  196. return;
  197. }
  198. const relatedComponent = utils.getRelatedComponent(node);
  199. if (
  200. relatedComponent
  201. && (componentUtil.isES6Component(relatedComponent.node, context) || (
  202. relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node)))
  203. && (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
  204. ) {
  205. reportErrorIfPropertyCasingTypo(node.parent.right, node.property, true);
  206. }
  207. },
  208. MethodDefinition(node) {
  209. if (!componentUtil.isES6Component(node.parent.parent, context)) {
  210. return;
  211. }
  212. reportErrorIfLifecycleMethodCasingTypo(node);
  213. },
  214. ObjectExpression(node) {
  215. const component = componentUtil.isES5Component(node, context) && components.get(node);
  216. if (!component) {
  217. return;
  218. }
  219. node.properties.filter((property) => property.type !== 'SpreadElement').forEach((property) => {
  220. reportErrorIfPropertyCasingTypo(property.value, property.key, false);
  221. reportErrorIfLifecycleMethodCasingTypo(property);
  222. });
  223. },
  224. };
  225. }),
  226. };