Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

require-default-props.js 21 KiB

há 3 anos
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. /**
  2. * @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
  3. * @author Vitor Balocco
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const Components = require('../util/Components');
  8. const variableUtil = require('../util/variable');
  9. const annotations = require('../util/annotations');
  10. const astUtil = require('../util/ast');
  11. const propsUtil = require('../util/props');
  12. const docsUrl = require('../util/docsUrl');
  13. const QUOTES_REGEX = /^["']|["']$/g;
  14. // ------------------------------------------------------------------------------
  15. // Rule Definition
  16. // ------------------------------------------------------------------------------
  17. module.exports = {
  18. meta: {
  19. docs: {
  20. description: 'Enforce a defaultProps definition for every prop that is not a required prop.',
  21. category: 'Best Practices',
  22. url: docsUrl('require-default-props')
  23. },
  24. schema: [{
  25. type: 'object',
  26. properties: {
  27. forbidDefaultForRequired: {
  28. type: 'boolean'
  29. }
  30. },
  31. additionalProperties: false
  32. }]
  33. },
  34. create: Components.detect((context, components, utils) => {
  35. const sourceCode = context.getSourceCode();
  36. const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
  37. const configuration = context.options[0] || {};
  38. const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
  39. // Used to track the type annotations in scope.
  40. // Necessary because babel's scopes do not track type annotations.
  41. let stack = null;
  42. /**
  43. * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
  44. * an Identifier, then the node is simply returned.
  45. * @param {ASTNode} node The node to resolve.
  46. * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
  47. */
  48. function resolveNodeValue(node) {
  49. if (node.type === 'Identifier') {
  50. return variableUtil.findVariableByName(context, node.name);
  51. }
  52. if (
  53. node.type === 'CallExpression' &&
  54. propWrapperFunctions.has(node.callee.name) &&
  55. node.arguments && node.arguments[0]
  56. ) {
  57. return resolveNodeValue(node.arguments[0]);
  58. }
  59. return node;
  60. }
  61. /**
  62. * Helper for accessing the current scope in the stack.
  63. * @param {string} key The name of the identifier to access. If omitted, returns the full scope.
  64. * @param {ASTNode} value If provided sets the new value for the identifier.
  65. * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
  66. */
  67. function typeScope(key, value) {
  68. if (arguments.length === 0) {
  69. return stack[stack.length - 1];
  70. } else if (arguments.length === 1) {
  71. return stack[stack.length - 1][key];
  72. }
  73. stack[stack.length - 1][key] = value;
  74. return value;
  75. }
  76. /**
  77. * Tries to find the definition of a GenericTypeAnnotation in the current scope.
  78. * @param {ASTNode} node The node GenericTypeAnnotation node to resolve.
  79. * @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise.
  80. */
  81. function resolveGenericTypeAnnotation(node) {
  82. if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') {
  83. return null;
  84. }
  85. return variableUtil.findVariableByName(context, node.id.name) || typeScope(node.id.name);
  86. }
  87. function resolveUnionTypeAnnotation(node) {
  88. // Go through all the union and resolve any generic types.
  89. return node.types.map(annotation => {
  90. if (annotation.type === 'GenericTypeAnnotation') {
  91. return resolveGenericTypeAnnotation(annotation);
  92. }
  93. return annotation;
  94. });
  95. }
  96. /**
  97. * Extracts a PropType from an ObjectExpression node.
  98. * @param {ASTNode} objectExpression ObjectExpression node.
  99. * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
  100. */
  101. function getPropTypesFromObjectExpression(objectExpression) {
  102. const props = objectExpression.properties.filter(property => property.type !== 'ExperimentalSpreadProperty' && property.type !== 'SpreadElement');
  103. return props.map(property => ({
  104. name: sourceCode.getText(property.key).replace(QUOTES_REGEX, ''),
  105. isRequired: propsUtil.isRequiredPropType(property.value),
  106. node: property
  107. }));
  108. }
  109. /**
  110. * Extracts a PropType from a TypeAnnotation node.
  111. * @param {ASTNode} node TypeAnnotation node.
  112. * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
  113. */
  114. function getPropTypesFromTypeAnnotation(node) {
  115. let properties;
  116. switch (node.typeAnnotation.type) {
  117. case 'GenericTypeAnnotation':
  118. let annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
  119. if (annotation && annotation.id) {
  120. annotation = variableUtil.findVariableByName(context, annotation.id.name);
  121. }
  122. properties = annotation ? (annotation.properties || []) : [];
  123. break;
  124. case 'UnionTypeAnnotation':
  125. const union = resolveUnionTypeAnnotation(node.typeAnnotation);
  126. properties = union.reduce((acc, curr) => {
  127. if (!curr) {
  128. return acc;
  129. }
  130. return acc.concat(curr.properties);
  131. }, []);
  132. break;
  133. case 'ObjectTypeAnnotation':
  134. properties = node.typeAnnotation.properties;
  135. break;
  136. default:
  137. properties = [];
  138. break;
  139. }
  140. const props = properties.filter(property => property.type === 'ObjectTypeProperty');
  141. return props.map(property => {
  142. // the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
  143. const tokens = context.getFirstTokens(property, 1);
  144. const name = tokens[0].value;
  145. return {
  146. name: name,
  147. isRequired: !property.optional,
  148. node: property
  149. };
  150. });
  151. }
  152. /**
  153. * Extracts a DefaultProp from an ObjectExpression node.
  154. * @param {ASTNode} objectExpression ObjectExpression node.
  155. * @returns {Object|string} Object representation of a defaultProp, to be consumed by
  156. * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
  157. * from this ObjectExpression can't be resolved.
  158. */
  159. function getDefaultPropsFromObjectExpression(objectExpression) {
  160. const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');
  161. if (hasSpread) {
  162. return 'unresolved';
  163. }
  164. return objectExpression.properties.map(property => sourceCode.getText(property.key).replace(QUOTES_REGEX, ''));
  165. }
  166. /**
  167. * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
  168. * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
  169. * without risking false negatives.
  170. * @param {Object} component The component to mark.
  171. * @returns {void}
  172. */
  173. function markDefaultPropsAsUnresolved(component) {
  174. components.set(component.node, {
  175. defaultProps: 'unresolved'
  176. });
  177. }
  178. /**
  179. * Adds propTypes to the component passed in.
  180. * @param {ASTNode} component The component to add the propTypes to.
  181. * @param {Object[]} propTypes propTypes to add to the component.
  182. * @returns {void}
  183. */
  184. function addPropTypesToComponent(component, propTypes) {
  185. const props = component.propTypes || [];
  186. components.set(component.node, {
  187. propTypes: props.concat(propTypes)
  188. });
  189. }
  190. /**
  191. * Adds defaultProps to the component passed in.
  192. * @param {ASTNode} component The component to add the defaultProps to.
  193. * @param {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved"
  194. * if this component has defaultProps that can't be resolved.
  195. * @returns {void}
  196. */
  197. function addDefaultPropsToComponent(component, defaultProps) {
  198. // Early return if this component's defaultProps is already marked as "unresolved".
  199. if (component.defaultProps === 'unresolved') {
  200. return;
  201. }
  202. if (defaultProps === 'unresolved') {
  203. markDefaultPropsAsUnresolved(component);
  204. return;
  205. }
  206. const defaults = component.defaultProps || {};
  207. defaultProps.forEach(defaultProp => {
  208. defaults[defaultProp] = true;
  209. });
  210. components.set(component.node, {
  211. defaultProps: defaults
  212. });
  213. }
  214. /**
  215. * Tries to find a props type annotation in a stateless component.
  216. * @param {ASTNode} node The AST node to look for a props type annotation.
  217. * @return {void}
  218. */
  219. function handleStatelessComponent(node) {
  220. if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
  221. return;
  222. }
  223. // find component this props annotation belongs to
  224. const component = components.get(utils.getParentStatelessComponent());
  225. if (!component) {
  226. return;
  227. }
  228. addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context));
  229. }
  230. function handlePropTypeAnnotationClassProperty(node) {
  231. // find component this props annotation belongs to
  232. const component = components.get(utils.getParentES6Component());
  233. if (!component) {
  234. return;
  235. }
  236. addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
  237. }
  238. function isPropTypeAnnotation(node) {
  239. return (astUtil.getPropertyName(node) === 'props' && !!node.typeAnnotation);
  240. }
  241. /**
  242. * Reports all propTypes passed in that don't have a defaultProp counterpart.
  243. * @param {Object[]} propTypes List of propTypes to check.
  244. * @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
  245. * @return {void}
  246. */
  247. function reportPropTypesWithoutDefault(propTypes, defaultProps) {
  248. // If this defaultProps is "unresolved", then we should ignore this component and not report
  249. // any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
  250. if (defaultProps === 'unresolved') {
  251. return;
  252. }
  253. propTypes.forEach(prop => {
  254. if (prop.isRequired) {
  255. if (forbidDefaultForRequired && defaultProps[prop.name]) {
  256. context.report(
  257. prop.node,
  258. 'propType "{{name}}" is required and should not have a defaultProp declaration.',
  259. {name: prop.name}
  260. );
  261. }
  262. return;
  263. }
  264. if (defaultProps[prop.name]) {
  265. return;
  266. }
  267. context.report(
  268. prop.node,
  269. 'propType "{{name}}" is not required, but has no corresponding defaultProp declaration.',
  270. {name: prop.name}
  271. );
  272. });
  273. }
  274. /**
  275. * Extracts a PropType from a TypeAnnotation contained in generic node.
  276. * @param {ASTNode} node TypeAnnotation node.
  277. * @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
  278. */
  279. function getPropTypesFromGeneric(node) {
  280. let annotation = resolveGenericTypeAnnotation(node);
  281. if (annotation && annotation.id) {
  282. annotation = variableUtil.findVariableByName(context, annotation.id.name);
  283. }
  284. const properties = annotation ? (annotation.properties || []) : [];
  285. const props = properties.filter(property => property.type === 'ObjectTypeProperty');
  286. return props.map(property => {
  287. // the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
  288. const tokens = context.getFirstTokens(property, 1);
  289. const name = tokens[0].value;
  290. return {
  291. name: name,
  292. isRequired: !property.optional,
  293. node: property
  294. };
  295. });
  296. }
  297. function hasPropTypesAsGeneric(node) {
  298. return node && node.parent && node.parent.type === 'ClassDeclaration';
  299. }
  300. function handlePropTypesAsGeneric(node) {
  301. const component = components.get(utils.getParentES6Component());
  302. if (!component) {
  303. return;
  304. }
  305. if (node.params[0]) {
  306. addPropTypesToComponent(component, getPropTypesFromGeneric(node.params[0], context));
  307. }
  308. }
  309. // --------------------------------------------------------------------------
  310. // Public API
  311. // --------------------------------------------------------------------------
  312. return {
  313. MemberExpression: function(node) {
  314. const isPropType = propsUtil.isPropTypesDeclaration(node);
  315. const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
  316. if (!isPropType && !isDefaultProp) {
  317. return;
  318. }
  319. // find component this propTypes/defaultProps belongs to
  320. const component = utils.getRelatedComponent(node);
  321. if (!component) {
  322. return;
  323. }
  324. // e.g.:
  325. // MyComponent.propTypes = {
  326. // foo: PropTypes.string.isRequired,
  327. // bar: PropTypes.string
  328. // };
  329. //
  330. // or:
  331. //
  332. // MyComponent.propTypes = myPropTypes;
  333. if (node.parent.type === 'AssignmentExpression') {
  334. const expression = resolveNodeValue(node.parent.right);
  335. if (!expression || expression.type !== 'ObjectExpression') {
  336. // If a value can't be found, we mark the defaultProps declaration as "unresolved", because
  337. // we should ignore this component and not report any errors for it, to avoid false-positives
  338. // with e.g. external defaultProps declarations.
  339. if (isDefaultProp) {
  340. markDefaultPropsAsUnresolved(component);
  341. }
  342. return;
  343. }
  344. if (isPropType) {
  345. addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
  346. } else {
  347. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  348. }
  349. return;
  350. }
  351. // e.g.:
  352. // MyComponent.propTypes.baz = PropTypes.string;
  353. if (node.parent.type === 'MemberExpression' && node.parent.parent.type === 'AssignmentExpression') {
  354. if (isPropType) {
  355. addPropTypesToComponent(component, [{
  356. name: node.parent.property.name,
  357. isRequired: propsUtil.isRequiredPropType(node.parent.parent.right),
  358. node: node.parent.parent
  359. }]);
  360. } else {
  361. addDefaultPropsToComponent(component, [node.parent.property.name]);
  362. }
  363. return;
  364. }
  365. },
  366. // e.g.:
  367. // class Hello extends React.Component {
  368. // static get propTypes() {
  369. // return {
  370. // name: PropTypes.string
  371. // };
  372. // }
  373. // static get defaultProps() {
  374. // return {
  375. // name: 'Dean'
  376. // };
  377. // }
  378. // render() {
  379. // return <div>Hello {this.props.name}</div>;
  380. // }
  381. // }
  382. MethodDefinition: function(node) {
  383. if (!node.static || node.kind !== 'get') {
  384. return;
  385. }
  386. const isPropType = propsUtil.isPropTypesDeclaration(node);
  387. const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
  388. if (!isPropType && !isDefaultProp) {
  389. return;
  390. }
  391. // find component this propTypes/defaultProps belongs to
  392. const component = components.get(utils.getParentES6Component());
  393. if (!component) {
  394. return;
  395. }
  396. const returnStatement = utils.findReturnStatement(node);
  397. if (!returnStatement) {
  398. return;
  399. }
  400. const expression = resolveNodeValue(returnStatement.argument);
  401. if (!expression || expression.type !== 'ObjectExpression') {
  402. return;
  403. }
  404. if (isPropType) {
  405. addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
  406. } else {
  407. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  408. }
  409. },
  410. // e.g.:
  411. // class Greeting extends React.Component {
  412. // render() {
  413. // return (
  414. // <h1>Hello, {this.props.foo} {this.props.bar}</h1>
  415. // );
  416. // }
  417. // static propTypes = {
  418. // foo: PropTypes.string,
  419. // bar: PropTypes.string.isRequired
  420. // };
  421. // }
  422. ClassProperty: function(node) {
  423. if (isPropTypeAnnotation(node)) {
  424. handlePropTypeAnnotationClassProperty(node);
  425. return;
  426. }
  427. if (!node.static) {
  428. return;
  429. }
  430. if (!node.value) {
  431. return;
  432. }
  433. const isPropType = astUtil.getPropertyName(node) === 'propTypes';
  434. const isDefaultProp = astUtil.getPropertyName(node) === 'defaultProps' || astUtil.getPropertyName(node) === 'getDefaultProps';
  435. if (!isPropType && !isDefaultProp) {
  436. return;
  437. }
  438. // find component this propTypes/defaultProps belongs to
  439. const component = components.get(utils.getParentES6Component());
  440. if (!component) {
  441. return;
  442. }
  443. const expression = resolveNodeValue(node.value);
  444. if (!expression || expression.type !== 'ObjectExpression') {
  445. return;
  446. }
  447. if (isPropType) {
  448. addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression));
  449. } else {
  450. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  451. }
  452. },
  453. // e.g.:
  454. // createReactClass({
  455. // render: function() {
  456. // return <div>{this.props.foo}</div>;
  457. // },
  458. // propTypes: {
  459. // foo: PropTypes.string.isRequired,
  460. // },
  461. // getDefaultProps: function() {
  462. // return {
  463. // foo: 'default'
  464. // };
  465. // }
  466. // });
  467. ObjectExpression: function(node) {
  468. // find component this propTypes/defaultProps belongs to
  469. const component = utils.isES5Component(node) && components.get(node);
  470. if (!component) {
  471. return;
  472. }
  473. // Search for the proptypes declaration
  474. node.properties.forEach(property => {
  475. if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
  476. return;
  477. }
  478. const isPropType = propsUtil.isPropTypesDeclaration(property);
  479. const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
  480. if (!isPropType && !isDefaultProp) {
  481. return;
  482. }
  483. if (isPropType && property.value.type === 'ObjectExpression') {
  484. addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value));
  485. return;
  486. }
  487. if (isDefaultProp && property.value.type === 'FunctionExpression') {
  488. const returnStatement = utils.findReturnStatement(property);
  489. if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
  490. return;
  491. }
  492. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
  493. }
  494. });
  495. },
  496. TypeAlias: function(node) {
  497. typeScope(node.id.name, node.right);
  498. },
  499. Program: function() {
  500. stack = [{}];
  501. },
  502. BlockStatement: function () {
  503. stack.push(Object.create(typeScope()));
  504. },
  505. 'BlockStatement:exit': function () {
  506. stack.pop();
  507. },
  508. // e.g.:
  509. // type HelloProps = {
  510. // foo?: string
  511. // };
  512. // class Hello extends React.Component<HelloProps> {
  513. // static defaultProps = {
  514. // foo: 'default'
  515. // }
  516. // render() {
  517. // return <div>{this.props.foo}</div>;
  518. // }
  519. // };
  520. TypeParameterInstantiation: function(node) {
  521. if (hasPropTypesAsGeneric(node)) {
  522. handlePropTypesAsGeneric(node);
  523. return;
  524. }
  525. },
  526. // Check for type annotations in stateless components
  527. FunctionDeclaration: handleStatelessComponent,
  528. ArrowFunctionExpression: handleStatelessComponent,
  529. FunctionExpression: handleStatelessComponent,
  530. 'Program:exit': function() {
  531. stack = null;
  532. const list = components.list();
  533. for (const component in list) {
  534. if (!has(list, component)) {
  535. continue;
  536. }
  537. // If no propTypes could be found, we don't report anything.
  538. if (!list[component].propTypes) {
  539. continue;
  540. }
  541. reportPropTypesWithoutDefault(
  542. list[component].propTypes,
  543. list[component].defaultProps || {}
  544. );
  545. }
  546. }
  547. };
  548. })
  549. };