[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Common defaultProps detection functionality.
|
---|
| 3 | */
|
---|
| 4 |
|
---|
| 5 | 'use strict';
|
---|
| 6 |
|
---|
| 7 | const fromEntries = require('object.fromentries');
|
---|
| 8 | const astUtil = require('./ast');
|
---|
| 9 | const componentUtil = require('./componentUtil');
|
---|
| 10 | const propsUtil = require('./props');
|
---|
| 11 | const variableUtil = require('./variable');
|
---|
| 12 | const propWrapperUtil = require('./propWrapper');
|
---|
| 13 | const getText = require('./eslint').getText;
|
---|
| 14 |
|
---|
| 15 | const QUOTES_REGEX = /^["']|["']$/g;
|
---|
| 16 |
|
---|
| 17 | module.exports = function defaultPropsInstructions(context, components, utils) {
|
---|
| 18 | /**
|
---|
| 19 | * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
|
---|
| 20 | * an Identifier, then the node is simply returned.
|
---|
| 21 | * @param {ASTNode} node The node to resolve.
|
---|
| 22 | * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
|
---|
| 23 | */
|
---|
| 24 | function resolveNodeValue(node) {
|
---|
| 25 | if (node.type === 'Identifier') {
|
---|
| 26 | return variableUtil.findVariableByName(context, node, node.name);
|
---|
| 27 | }
|
---|
| 28 | if (
|
---|
| 29 | node.type === 'CallExpression'
|
---|
| 30 | && propWrapperUtil.isPropWrapperFunction(context, node.callee.name)
|
---|
| 31 | && node.arguments && node.arguments[0]
|
---|
| 32 | ) {
|
---|
| 33 | return resolveNodeValue(node.arguments[0]);
|
---|
| 34 | }
|
---|
| 35 | return node;
|
---|
| 36 | }
|
---|
| 37 |
|
---|
| 38 | /**
|
---|
| 39 | * Extracts a DefaultProp from an ObjectExpression node.
|
---|
| 40 | * @param {ASTNode} objectExpression ObjectExpression node.
|
---|
| 41 | * @returns {Object|string} Object representation of a defaultProp, to be consumed by
|
---|
| 42 | * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
|
---|
| 43 | * from this ObjectExpression can't be resolved.
|
---|
| 44 | */
|
---|
| 45 | function getDefaultPropsFromObjectExpression(objectExpression) {
|
---|
| 46 | const hasSpread = objectExpression.properties.find((property) => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');
|
---|
| 47 |
|
---|
| 48 | if (hasSpread) {
|
---|
| 49 | return 'unresolved';
|
---|
| 50 | }
|
---|
| 51 |
|
---|
| 52 | return objectExpression.properties.map((defaultProp) => ({
|
---|
| 53 | name: getText(context, defaultProp.key).replace(QUOTES_REGEX, ''),
|
---|
| 54 | node: defaultProp,
|
---|
| 55 | }));
|
---|
| 56 | }
|
---|
| 57 |
|
---|
| 58 | /**
|
---|
| 59 | * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
|
---|
| 60 | * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
|
---|
| 61 | * without risking false negatives.
|
---|
| 62 | * @param {Object} component The component to mark.
|
---|
| 63 | * @returns {void}
|
---|
| 64 | */
|
---|
| 65 | function markDefaultPropsAsUnresolved(component) {
|
---|
| 66 | components.set(component.node, {
|
---|
| 67 | defaultProps: 'unresolved',
|
---|
| 68 | });
|
---|
| 69 | }
|
---|
| 70 |
|
---|
| 71 | /**
|
---|
| 72 | * Adds defaultProps to the component passed in.
|
---|
| 73 | * @param {ASTNode} component The component to add the defaultProps to.
|
---|
| 74 | * @param {Object[]|'unresolved'} defaultProps defaultProps to add to the component or the string "unresolved"
|
---|
| 75 | * if this component has defaultProps that can't be resolved.
|
---|
| 76 | * @returns {void}
|
---|
| 77 | */
|
---|
| 78 | function addDefaultPropsToComponent(component, defaultProps) {
|
---|
| 79 | // Early return if this component's defaultProps is already marked as "unresolved".
|
---|
| 80 | if (component.defaultProps === 'unresolved') {
|
---|
| 81 | return;
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | if (defaultProps === 'unresolved') {
|
---|
| 85 | markDefaultPropsAsUnresolved(component);
|
---|
| 86 | return;
|
---|
| 87 | }
|
---|
| 88 |
|
---|
| 89 | const defaults = component.defaultProps || {};
|
---|
| 90 | const newDefaultProps = Object.assign(
|
---|
| 91 | {},
|
---|
| 92 | defaults,
|
---|
| 93 | fromEntries(defaultProps.map((prop) => [prop.name, prop]))
|
---|
| 94 | );
|
---|
| 95 |
|
---|
| 96 | components.set(component.node, {
|
---|
| 97 | defaultProps: newDefaultProps,
|
---|
| 98 | });
|
---|
| 99 | }
|
---|
| 100 |
|
---|
| 101 | return {
|
---|
| 102 | MemberExpression(node) {
|
---|
| 103 | const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
|
---|
| 104 |
|
---|
| 105 | if (!isDefaultProp) {
|
---|
| 106 | return;
|
---|
| 107 | }
|
---|
| 108 |
|
---|
| 109 | // find component this defaultProps belongs to
|
---|
| 110 | const component = utils.getRelatedComponent(node);
|
---|
| 111 | if (!component) {
|
---|
| 112 | return;
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | // e.g.:
|
---|
| 116 | // MyComponent.propTypes = {
|
---|
| 117 | // foo: React.PropTypes.string.isRequired,
|
---|
| 118 | // bar: React.PropTypes.string
|
---|
| 119 | // };
|
---|
| 120 | //
|
---|
| 121 | // or:
|
---|
| 122 | //
|
---|
| 123 | // MyComponent.propTypes = myPropTypes;
|
---|
| 124 | if (node.parent.type === 'AssignmentExpression') {
|
---|
| 125 | const expression = resolveNodeValue(node.parent.right);
|
---|
| 126 | if (!expression || expression.type !== 'ObjectExpression') {
|
---|
| 127 | // If a value can't be found, we mark the defaultProps declaration as "unresolved", because
|
---|
| 128 | // we should ignore this component and not report any errors for it, to avoid false-positives
|
---|
| 129 | // with e.g. external defaultProps declarations.
|
---|
| 130 | if (isDefaultProp) {
|
---|
| 131 | markDefaultPropsAsUnresolved(component);
|
---|
| 132 | }
|
---|
| 133 |
|
---|
| 134 | return;
|
---|
| 135 | }
|
---|
| 136 |
|
---|
| 137 | addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
---|
| 138 |
|
---|
| 139 | return;
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | // e.g.:
|
---|
| 143 | // MyComponent.propTypes.baz = React.PropTypes.string;
|
---|
| 144 | if (node.parent.type === 'MemberExpression' && node.parent.parent
|
---|
| 145 | && node.parent.parent.type === 'AssignmentExpression') {
|
---|
| 146 | addDefaultPropsToComponent(component, [{
|
---|
| 147 | name: node.parent.property.name,
|
---|
| 148 | node: node.parent.parent,
|
---|
| 149 | }]);
|
---|
| 150 | }
|
---|
| 151 | },
|
---|
| 152 |
|
---|
| 153 | // e.g.:
|
---|
| 154 | // class Hello extends React.Component {
|
---|
| 155 | // static get defaultProps() {
|
---|
| 156 | // return {
|
---|
| 157 | // name: 'Dean'
|
---|
| 158 | // };
|
---|
| 159 | // }
|
---|
| 160 | // render() {
|
---|
| 161 | // return <div>Hello {this.props.name}</div>;
|
---|
| 162 | // }
|
---|
| 163 | // }
|
---|
| 164 | MethodDefinition(node) {
|
---|
| 165 | if (!node.static || node.kind !== 'get') {
|
---|
| 166 | return;
|
---|
| 167 | }
|
---|
| 168 |
|
---|
| 169 | if (!propsUtil.isDefaultPropsDeclaration(node)) {
|
---|
| 170 | return;
|
---|
| 171 | }
|
---|
| 172 |
|
---|
| 173 | // find component this propTypes/defaultProps belongs to
|
---|
| 174 | const component = components.get(componentUtil.getParentES6Component(context, node));
|
---|
| 175 | if (!component) {
|
---|
| 176 | return;
|
---|
| 177 | }
|
---|
| 178 |
|
---|
| 179 | const returnStatement = utils.findReturnStatement(node);
|
---|
| 180 | if (!returnStatement) {
|
---|
| 181 | return;
|
---|
| 182 | }
|
---|
| 183 |
|
---|
| 184 | const expression = resolveNodeValue(returnStatement.argument);
|
---|
| 185 | if (!expression || expression.type !== 'ObjectExpression') {
|
---|
| 186 | return;
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
---|
| 190 | },
|
---|
| 191 |
|
---|
| 192 | // e.g.:
|
---|
| 193 | // class Greeting extends React.Component {
|
---|
| 194 | // render() {
|
---|
| 195 | // return (
|
---|
| 196 | // <h1>Hello, {this.props.foo} {this.props.bar}</h1>
|
---|
| 197 | // );
|
---|
| 198 | // }
|
---|
| 199 | // static defaultProps = {
|
---|
| 200 | // foo: 'bar',
|
---|
| 201 | // bar: 'baz'
|
---|
| 202 | // };
|
---|
| 203 | // }
|
---|
| 204 | 'ClassProperty, PropertyDefinition'(node) {
|
---|
| 205 | if (!(node.static && node.value)) {
|
---|
| 206 | return;
|
---|
| 207 | }
|
---|
| 208 |
|
---|
| 209 | const propName = astUtil.getPropertyName(node);
|
---|
| 210 | const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
|
---|
| 211 |
|
---|
| 212 | if (!isDefaultProp) {
|
---|
| 213 | return;
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | // find component this propTypes/defaultProps belongs to
|
---|
| 217 | const component = components.get(componentUtil.getParentES6Component(context, node));
|
---|
| 218 | if (!component) {
|
---|
| 219 | return;
|
---|
| 220 | }
|
---|
| 221 |
|
---|
| 222 | const expression = resolveNodeValue(node.value);
|
---|
| 223 | if (!expression || expression.type !== 'ObjectExpression') {
|
---|
| 224 | return;
|
---|
| 225 | }
|
---|
| 226 |
|
---|
| 227 | addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
---|
| 228 | },
|
---|
| 229 |
|
---|
| 230 | // e.g.:
|
---|
| 231 | // React.createClass({
|
---|
| 232 | // render: function() {
|
---|
| 233 | // return <div>{this.props.foo}</div>;
|
---|
| 234 | // },
|
---|
| 235 | // getDefaultProps: function() {
|
---|
| 236 | // return {
|
---|
| 237 | // foo: 'default'
|
---|
| 238 | // };
|
---|
| 239 | // }
|
---|
| 240 | // });
|
---|
| 241 | ObjectExpression(node) {
|
---|
| 242 | // find component this propTypes/defaultProps belongs to
|
---|
| 243 | const component = componentUtil.isES5Component(node, context) && components.get(node);
|
---|
| 244 | if (!component) {
|
---|
| 245 | return;
|
---|
| 246 | }
|
---|
| 247 |
|
---|
| 248 | // Search for the proptypes declaration
|
---|
| 249 | node.properties.forEach((property) => {
|
---|
| 250 | if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
|
---|
| 251 | return;
|
---|
| 252 | }
|
---|
| 253 |
|
---|
| 254 | const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
|
---|
| 255 |
|
---|
| 256 | if (isDefaultProp && property.value.type === 'FunctionExpression') {
|
---|
| 257 | const returnStatement = utils.findReturnStatement(property);
|
---|
| 258 | if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
|
---|
| 259 | return;
|
---|
| 260 | }
|
---|
| 261 |
|
---|
| 262 | addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
|
---|
| 263 | }
|
---|
| 264 | });
|
---|
| 265 | },
|
---|
| 266 | };
|
---|
| 267 | };
|
---|