init source
This commit is contained in:
+905
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* @fileoverview Utility class and functions for React components detection
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const doctrine = require('doctrine');
|
||||
const arrayIncludes = require('array-includes');
|
||||
const values = require('object.values');
|
||||
|
||||
const variableUtil = require('./variable');
|
||||
const pragmaUtil = require('./pragma');
|
||||
const astUtil = require('./ast');
|
||||
const propTypesUtil = require('./propTypes');
|
||||
const jsxUtil = require('./jsx');
|
||||
const usedPropTypesUtil = require('./usedPropTypes');
|
||||
const defaultPropsUtil = require('./defaultProps');
|
||||
|
||||
function getId(node) {
|
||||
return node && node.range.join(':');
|
||||
}
|
||||
|
||||
function usedPropTypesAreEquivalent(propA, propB) {
|
||||
if (propA.name === propB.name) {
|
||||
if (!propA.allNames && !propB.allNames) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mergeUsedPropTypes(propsList, newPropsList) {
|
||||
const propsToAdd = [];
|
||||
newPropsList.forEach((newProp) => {
|
||||
const newPropisAlreadyInTheList = propsList.some(prop => usedPropTypesAreEquivalent(prop, newProp));
|
||||
if (!newPropisAlreadyInTheList) {
|
||||
propsToAdd.push(newProp);
|
||||
}
|
||||
});
|
||||
|
||||
return propsList.concat(propsToAdd);
|
||||
}
|
||||
|
||||
const Lists = new WeakMap();
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
class Components {
|
||||
constructor() {
|
||||
Lists.set(this, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the components list, or update it if it's already in the list
|
||||
*
|
||||
* @param {ASTNode} node The AST node being added.
|
||||
* @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
|
||||
* @returns {Object} Added component object
|
||||
*/
|
||||
add(node, confidence) {
|
||||
const id = getId(node);
|
||||
const list = Lists.get(this);
|
||||
if (list[id]) {
|
||||
if (confidence === 0 || list[id].confidence === 0) {
|
||||
list[id].confidence = 0;
|
||||
} else {
|
||||
list[id].confidence = Math.max(list[id].confidence, confidence);
|
||||
}
|
||||
return list[id];
|
||||
}
|
||||
list[id] = {
|
||||
node,
|
||||
confidence
|
||||
};
|
||||
return list[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a component in the list using its node
|
||||
*
|
||||
* @param {ASTNode} node The AST node being searched.
|
||||
* @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
|
||||
*/
|
||||
get(node) {
|
||||
const id = getId(node);
|
||||
const item = Lists.get(this)[id];
|
||||
if (item && item.confidence >= 1) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a component in the list
|
||||
*
|
||||
* @param {ASTNode} node The AST node being updated.
|
||||
* @param {Object} props Additional properties to add to the component.
|
||||
*/
|
||||
set(node, props) {
|
||||
const list = Lists.get(this);
|
||||
let component = list[getId(node)];
|
||||
while (!component) {
|
||||
node = node.parent;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
component = list[getId(node)];
|
||||
}
|
||||
|
||||
Object.assign(
|
||||
component,
|
||||
props,
|
||||
{
|
||||
usedPropTypes: mergeUsedPropTypes(
|
||||
component.usedPropTypes || [],
|
||||
props.usedPropTypes || []
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the components list
|
||||
* Components for which we are not confident are not returned
|
||||
*
|
||||
* @returns {Object} Components list
|
||||
*/
|
||||
list() {
|
||||
const thisList = Lists.get(this);
|
||||
const list = {};
|
||||
const usedPropTypes = {};
|
||||
|
||||
// Find props used in components for which we are not confident
|
||||
Object.keys(thisList).filter(i => thisList[i].confidence < 2).forEach((i) => {
|
||||
let component = null;
|
||||
let node = null;
|
||||
node = thisList[i].node;
|
||||
while (!component && node.parent) {
|
||||
node = node.parent;
|
||||
// Stop moving up if we reach a decorator
|
||||
if (node.type === 'Decorator') {
|
||||
break;
|
||||
}
|
||||
component = this.get(node);
|
||||
}
|
||||
if (component) {
|
||||
const newUsedProps = (thisList[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init');
|
||||
|
||||
const componentId = getId(component.node);
|
||||
|
||||
usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
|
||||
}
|
||||
});
|
||||
|
||||
// Assign used props in not confident components to the parent component
|
||||
Object.keys(thisList).filter(j => thisList[j].confidence >= 2).forEach((j) => {
|
||||
const id = getId(thisList[j].node);
|
||||
list[j] = thisList[j];
|
||||
if (usedPropTypes[id]) {
|
||||
list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of the components list
|
||||
* Components for which we are not confident are not counted
|
||||
*
|
||||
* @returns {Number} Components list length
|
||||
*/
|
||||
length() {
|
||||
const list = Lists.get(this);
|
||||
return Object.keys(list).filter(i => list[i].confidence >= 2).length;
|
||||
}
|
||||
}
|
||||
|
||||
function componentRule(rule, context) {
|
||||
const createClass = pragmaUtil.getCreateClassFromContext(context);
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
const sourceCode = context.getSourceCode();
|
||||
const components = new Components();
|
||||
|
||||
// Utilities for component detection
|
||||
const utils = {
|
||||
|
||||
/**
|
||||
* Check if the node is a React ES5 component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if the node is a React ES5 component, false if not
|
||||
*/
|
||||
isES5Component(node) {
|
||||
if (!node.parent) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is a React ES6 component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if the node is a React ES6 component, false if not
|
||||
*/
|
||||
isES6Component(node) {
|
||||
if (utils.isExplicitComponent(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!node.superClass) {
|
||||
return false;
|
||||
}
|
||||
return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is explicitly declared as a descendant of a React Component
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
|
||||
* @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
|
||||
*/
|
||||
isExplicitComponent(node) {
|
||||
let comment;
|
||||
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
|
||||
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
|
||||
try {
|
||||
comment = sourceCode.getJSDocComment(node);
|
||||
} catch (e) {
|
||||
comment = null;
|
||||
}
|
||||
|
||||
if (comment === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commentAst = doctrine.parse(comment.value, {
|
||||
unwrap: true,
|
||||
tags: ['extends', 'augments']
|
||||
});
|
||||
|
||||
const relevantTags = commentAst.tags.filter(tag => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
|
||||
|
||||
return relevantTags.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if our component extends React.PureComponent
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if node extends React.PureComponent, false if not
|
||||
*/
|
||||
isPureComponent(node) {
|
||||
if (node.superClass) {
|
||||
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if variable is destructured from pragma import
|
||||
*
|
||||
* @param {string} variable The variable name to check
|
||||
* @returns {Boolean} True if createElement is destructured from the pragma
|
||||
*/
|
||||
isDestructuredFromPragmaImport(variable) {
|
||||
const variables = variableUtil.variablesInScope(context);
|
||||
const variableInScope = variableUtil.getVariable(variables, variable);
|
||||
if (variableInScope) {
|
||||
const map = variableInScope.scope.set;
|
||||
return map.has(pragma);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks to see if node is called within createElement from pragma
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if createElement called from pragma
|
||||
*/
|
||||
isCreateElement(node) {
|
||||
const calledOnPragma = (
|
||||
node &&
|
||||
node.callee &&
|
||||
node.callee.object &&
|
||||
node.callee.object.name === pragma &&
|
||||
node.callee.property &&
|
||||
node.callee.property.name === 'createElement'
|
||||
);
|
||||
|
||||
const calledDirectly = (
|
||||
node &&
|
||||
node.callee &&
|
||||
node.callee.name === 'createElement'
|
||||
);
|
||||
|
||||
if (this.isDestructuredFromPragmaImport('createElement')) {
|
||||
return calledDirectly || calledOnPragma;
|
||||
}
|
||||
return calledOnPragma;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we are in a class constructor
|
||||
* @return {boolean} true if we are in a class constructor, false if not
|
||||
*/
|
||||
inConstructor() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
|
||||
return true;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the node is MemberExpression of `this.state`
|
||||
* @param {Object} node The node to process
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isStateMemberExpression(node) {
|
||||
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
|
||||
},
|
||||
|
||||
getReturnPropertyAndNode(ASTnode) {
|
||||
let property;
|
||||
let node = ASTnode;
|
||||
switch (node.type) {
|
||||
case 'ReturnStatement':
|
||||
property = 'argument';
|
||||
break;
|
||||
case 'ArrowFunctionExpression':
|
||||
property = 'body';
|
||||
if (node[property] && node[property].type === 'BlockStatement') {
|
||||
node = utils.findReturnStatement(node);
|
||||
property = 'argument';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
node = utils.findReturnStatement(node);
|
||||
property = 'argument';
|
||||
}
|
||||
return {
|
||||
node,
|
||||
property
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning JSX
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
|
||||
* @returns {Boolean} True if the node is returning JSX, false if not
|
||||
*/
|
||||
isReturningJSX(ASTnode, strict) {
|
||||
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
|
||||
const node = nodeAndProperty.node;
|
||||
const property = nodeAndProperty.property;
|
||||
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const returnsConditionalJSXConsequent = node[property] &&
|
||||
node[property].type === 'ConditionalExpression' &&
|
||||
jsxUtil.isJSX(node[property].consequent);
|
||||
const returnsConditionalJSXAlternate = node[property] &&
|
||||
node[property].type === 'ConditionalExpression' &&
|
||||
jsxUtil.isJSX(node[property].alternate);
|
||||
const returnsConditionalJSX = strict ?
|
||||
(returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) :
|
||||
(returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
|
||||
|
||||
const returnsJSX = node[property] &&
|
||||
jsxUtil.isJSX(node[property]);
|
||||
const returnsPragmaCreateElement = this.isCreateElement(node[property]);
|
||||
|
||||
return Boolean(
|
||||
returnsConditionalJSX ||
|
||||
returnsJSX ||
|
||||
returnsPragmaCreateElement
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning null
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
* @returns {Boolean} True if the node is returning null, false if not
|
||||
*/
|
||||
isReturningNull(ASTnode) {
|
||||
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
|
||||
const property = nodeAndProperty.property;
|
||||
const node = nodeAndProperty.node;
|
||||
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node[property] && node[property].value === null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the node is returning JSX or null
|
||||
*
|
||||
* @param {ASTNode} ASTNode The AST node being checked
|
||||
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
|
||||
* @returns {Boolean} True if the node is returning JSX or null, false if not
|
||||
*/
|
||||
isReturningJSXOrNull(ASTNode, strict) {
|
||||
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
|
||||
},
|
||||
|
||||
getPragmaComponentWrapper(node) {
|
||||
let isPragmaComponentWrapper;
|
||||
let currentNode = node;
|
||||
let prevNode;
|
||||
do {
|
||||
currentNode = currentNode.parent;
|
||||
isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
|
||||
if (isPragmaComponentWrapper) {
|
||||
prevNode = currentNode;
|
||||
}
|
||||
} while (isPragmaComponentWrapper);
|
||||
|
||||
return prevNode;
|
||||
},
|
||||
|
||||
getComponentNameFromJSXElement(node) {
|
||||
if (node.type !== 'JSXElement') {
|
||||
return null;
|
||||
}
|
||||
if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
|
||||
return node.openingElement.name.name;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getting the first JSX element's name.
|
||||
* @param {object} node
|
||||
* @returns {string | null}
|
||||
*/
|
||||
getNameOfWrappedComponent(node) {
|
||||
if (node.length < 1) {
|
||||
return null;
|
||||
}
|
||||
const body = node[0].body;
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
if (body.type === 'JSXElement') {
|
||||
return this.getComponentNameFromJSXElement(body);
|
||||
}
|
||||
if (body.type === 'BlockStatement') {
|
||||
const jsxElement = body.body.find(item => item.type === 'ReturnStatement');
|
||||
return jsxElement && this.getComponentNameFromJSXElement(jsxElement.argument);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the list of names of components created till now
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
getDetectedComponents() {
|
||||
const list = components.list();
|
||||
return values(list).filter((val) => {
|
||||
if (val.node.type === 'ClassDeclaration') {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
val.node.type === 'ArrowFunctionExpression' &&
|
||||
val.node.parent &&
|
||||
val.node.parent.type === 'VariableDeclarator' &&
|
||||
val.node.parent.id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map((val) => {
|
||||
if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
|
||||
return val.node.id.name;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* It will check wheater memo/forwardRef is wrapping existing component or
|
||||
* creating a new one.
|
||||
* @param {object} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
nodeWrapsComponent(node) {
|
||||
const childComponent = this.getNameOfWrappedComponent(node.arguments);
|
||||
const componentList = this.getDetectedComponents();
|
||||
return !!childComponent && arrayIncludes(componentList, childComponent);
|
||||
},
|
||||
|
||||
isPragmaComponentWrapper(node) {
|
||||
if (!node || node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const propertyNames = ['forwardRef', 'memo'];
|
||||
const calleeObject = node.callee.object;
|
||||
if (calleeObject && node.callee.property) {
|
||||
return arrayIncludes(propertyNames, node.callee.property.name) &&
|
||||
calleeObject.name === pragma &&
|
||||
!this.nodeWrapsComponent(node);
|
||||
}
|
||||
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a return statment in the current node
|
||||
*
|
||||
* @param {ASTNode} ASTnode The AST node being checked
|
||||
*/
|
||||
findReturnStatement: astUtil.findReturnStatement,
|
||||
|
||||
/**
|
||||
* Get the parent component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentComponent() {
|
||||
return (
|
||||
utils.getParentES6Component() ||
|
||||
utils.getParentES5Component() ||
|
||||
utils.getParentStatelessComponent()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent ES5 component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentES5Component() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
const node = scope.block && scope.block.parent && scope.block.parent.parent;
|
||||
if (node && utils.isES5Component(node)) {
|
||||
return node;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent ES6 component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentES6Component() {
|
||||
let scope = context.getScope();
|
||||
while (scope && scope.type !== 'class') {
|
||||
scope = scope.upper;
|
||||
}
|
||||
const node = scope && scope.block;
|
||||
if (!node || !utils.isES6Component(node)) {
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInAllowedPositionForComponent(node) {
|
||||
switch (node.parent.type) {
|
||||
case 'VariableDeclarator':
|
||||
case 'AssignmentExpression':
|
||||
case 'Property':
|
||||
case 'ReturnStatement':
|
||||
case 'ExportDefaultDeclaration': {
|
||||
return true;
|
||||
}
|
||||
case 'SequenceExpression': {
|
||||
return utils.isInAllowedPositionForComponent(node.parent) &&
|
||||
node === node.parent.expressions[node.parent.expressions.length - 1];
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get node if node is a stateless component, or node.parent in cases like
|
||||
* `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
|
||||
* @param {ASTNode} node
|
||||
* @returns {ASTNode | undefined}
|
||||
*/
|
||||
getStatelessComponent(node) {
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
if (utils.isReturningJSXOrNull(node)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
||||
if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
|
||||
const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
|
||||
if (pragmaComponentWrapper) {
|
||||
return pragmaComponentWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent stateless component node from the current scope
|
||||
*
|
||||
* @returns {ASTNode} component node, null if we are not in a component
|
||||
*/
|
||||
getParentStatelessComponent() {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
const node = scope.block;
|
||||
const statelessComponent = utils.getStatelessComponent(node);
|
||||
if (statelessComponent) {
|
||||
return statelessComponent;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the related component from a node
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked (must be a MemberExpression).
|
||||
* @returns {ASTNode} component node, null if we cannot find the component
|
||||
*/
|
||||
getRelatedComponent(node) {
|
||||
let i;
|
||||
let j;
|
||||
let k;
|
||||
let l;
|
||||
let componentNode;
|
||||
// Get the component path
|
||||
const componentPath = [];
|
||||
while (node) {
|
||||
if (node.property && node.property.type === 'Identifier') {
|
||||
componentPath.push(node.property.name);
|
||||
}
|
||||
if (node.object && node.object.type === 'Identifier') {
|
||||
componentPath.push(node.object.name);
|
||||
}
|
||||
node = node.object;
|
||||
}
|
||||
componentPath.reverse();
|
||||
const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
|
||||
|
||||
// Find the variable in the current scope
|
||||
const variableName = componentPath.shift();
|
||||
if (!variableName) {
|
||||
return null;
|
||||
}
|
||||
let variableInScope;
|
||||
const variables = variableUtil.variablesInScope(context);
|
||||
for (i = 0, j = variables.length; i < j; i++) {
|
||||
if (variables[i].name === variableName) {
|
||||
variableInScope = variables[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!variableInScope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find the component using variable references
|
||||
const refs = variableInScope.references;
|
||||
refs.some((ref) => {
|
||||
let refId = ref.identifier;
|
||||
if (refId.parent && refId.parent.type === 'MemberExpression') {
|
||||
refId = refId.parent;
|
||||
}
|
||||
if (sourceCode.getText(refId) !== componentName) {
|
||||
return false;
|
||||
}
|
||||
if (refId.type === 'MemberExpression') {
|
||||
componentNode = refId.parent.right;
|
||||
} else if (
|
||||
refId.parent &&
|
||||
refId.parent.type === 'VariableDeclarator' &&
|
||||
refId.parent.init &&
|
||||
refId.parent.init.type !== 'Identifier'
|
||||
) {
|
||||
componentNode = refId.parent.init;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (componentNode) {
|
||||
// Return the component
|
||||
return components.add(componentNode, 1);
|
||||
}
|
||||
|
||||
// Try to find the component using variable declarations
|
||||
const defs = variableInScope.defs;
|
||||
const defInScope = defs.find(def => (
|
||||
def.type === 'ClassName' ||
|
||||
def.type === 'FunctionName' ||
|
||||
def.type === 'Variable'
|
||||
));
|
||||
if (!defInScope || !defInScope.node) {
|
||||
return null;
|
||||
}
|
||||
componentNode = defInScope.node.init || defInScope.node;
|
||||
|
||||
// Traverse the node properties to the component declaration
|
||||
for (i = 0, j = componentPath.length; i < j; i++) {
|
||||
if (!componentNode.properties) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
for (k = 0, l = componentNode.properties.length; k < l; k++) {
|
||||
if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
|
||||
componentNode = componentNode.properties[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!componentNode || !componentNode.value) {
|
||||
return null;
|
||||
}
|
||||
componentNode = componentNode.value;
|
||||
}
|
||||
|
||||
// Return the component
|
||||
return components.add(componentNode, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Component detection instructions
|
||||
const detectionInstructions = {
|
||||
CallExpression(node) {
|
||||
if (!utils.isPragmaComponentWrapper(node)) {
|
||||
return;
|
||||
}
|
||||
if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
||||
components.add(node, 2);
|
||||
}
|
||||
},
|
||||
|
||||
ClassExpression(node) {
|
||||
if (!utils.isES6Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
if (!utils.isES6Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ClassProperty(node) {
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!utils.isES5Component(node)) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
const component = utils.getParentComponent();
|
||||
if (
|
||||
!component ||
|
||||
(component.parent && component.parent.type === 'JSXExpressionContainer')
|
||||
) {
|
||||
// Ban the node if we cannot find a parent component
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
components.add(component, 1);
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
components.add(node, 1);
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
if (node.async) {
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
const component = utils.getParentComponent();
|
||||
if (
|
||||
!component ||
|
||||
(component.parent && component.parent.type === 'JSXExpressionContainer')
|
||||
) {
|
||||
// Ban the node if we cannot find a parent component
|
||||
components.add(node, 0);
|
||||
return;
|
||||
}
|
||||
if (component.expression && utils.isReturningJSX(component)) {
|
||||
components.add(component, 2);
|
||||
} else {
|
||||
components.add(component, 1);
|
||||
}
|
||||
},
|
||||
|
||||
ThisExpression(node) {
|
||||
const component = utils.getParentComponent();
|
||||
if (!component || !/Function/.test(component.type) || !node.parent.property) {
|
||||
return;
|
||||
}
|
||||
// Ban functions accessing a property on a ThisExpression
|
||||
components.add(node, 0);
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (!utils.isReturningJSX(node)) {
|
||||
return;
|
||||
}
|
||||
node = utils.getParentComponent();
|
||||
if (!node) {
|
||||
const scope = context.getScope();
|
||||
components.add(scope.block, 1);
|
||||
return;
|
||||
}
|
||||
components.add(node, 2);
|
||||
}
|
||||
};
|
||||
|
||||
// Update the provided rule instructions to add the component detection
|
||||
const ruleInstructions = rule(context, components, utils);
|
||||
const updatedRuleInstructions = Object.assign({}, ruleInstructions);
|
||||
const propTypesInstructions = propTypesUtil(context, components, utils);
|
||||
const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
|
||||
const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
|
||||
const allKeys = new Set(Object.keys(detectionInstructions).concat(
|
||||
Object.keys(propTypesInstructions),
|
||||
Object.keys(usedPropTypesInstructions),
|
||||
Object.keys(defaultPropsInstructions)
|
||||
));
|
||||
|
||||
allKeys.forEach((instruction) => {
|
||||
updatedRuleInstructions[instruction] = function (node) {
|
||||
if (instruction in detectionInstructions) {
|
||||
detectionInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in propTypesInstructions) {
|
||||
propTypesInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in usedPropTypesInstructions) {
|
||||
usedPropTypesInstructions[instruction](node);
|
||||
}
|
||||
if (instruction in defaultPropsInstructions) {
|
||||
defaultPropsInstructions[instruction](node);
|
||||
}
|
||||
if (ruleInstructions[instruction]) {
|
||||
return ruleInstructions[instruction](node);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Return the updated rule instructions
|
||||
return updatedRuleInstructions;
|
||||
}
|
||||
|
||||
module.exports = Object.assign(Components, {
|
||||
detect(rule) {
|
||||
return componentRule.bind(this, rule);
|
||||
}
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for type annotation detection.
|
||||
* @author Yannick Croissant
|
||||
* @author Vitor Balocco
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a `props` argument with a flow type annotation.
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {Object} context
|
||||
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
|
||||
*/
|
||||
function isAnnotatedFunctionPropsDeclaration(node, context) {
|
||||
if (!node || !node.params || !node.params.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const typeNode = node.params[0].type === 'AssignmentPattern' ? node.params[0].left : node.params[0];
|
||||
|
||||
const tokens = context.getFirstTokens(typeNode, 2);
|
||||
const isAnnotated = typeNode.typeAnnotation;
|
||||
const isDestructuredProps = typeNode.type === 'ObjectPattern';
|
||||
const isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props');
|
||||
|
||||
return (isAnnotated && (isDestructuredProps || isProps));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAnnotatedFunctionPropsDeclaration
|
||||
};
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for AST
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Find a return statment in the current node
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {ASTNode | false}
|
||||
*/
|
||||
function findReturnStatement(node) {
|
||||
if (
|
||||
(!node.value || !node.value.body || !node.value.body.body) &&
|
||||
(!node.body || !node.body.body)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyNodes = (node.value ? node.value.body.body : node.body.body);
|
||||
|
||||
return (function loopNodes(nodes) {
|
||||
let i = nodes.length - 1;
|
||||
for (; i >= 0; i--) {
|
||||
if (nodes[i].type === 'ReturnStatement') {
|
||||
return nodes[i];
|
||||
}
|
||||
if (nodes[i].type === 'SwitchStatement') {
|
||||
let j = nodes[i].cases.length - 1;
|
||||
for (; j >= 0; j--) {
|
||||
return loopNodes(nodes[i].cases[j].consequent);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}(bodyNodes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node with property's name
|
||||
* @param {Object} node - Property.
|
||||
* @returns {Object} Property name node.
|
||||
*/
|
||||
function getPropertyNameNode(node) {
|
||||
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
|
||||
return node.key;
|
||||
}
|
||||
if (node.type === 'MemberExpression') {
|
||||
return node.property;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties name
|
||||
* @param {Object} node - Property.
|
||||
* @returns {String} Property name.
|
||||
*/
|
||||
function getPropertyName(node) {
|
||||
const nameNode = getPropertyNameNode(node);
|
||||
return nameNode ? nameNode.name : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties for a given AST node
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Array} Properties array.
|
||||
*/
|
||||
function getComponentProperties(node) {
|
||||
switch (node.type) {
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression':
|
||||
return node.body.body;
|
||||
case 'ObjectExpression':
|
||||
return node.properties;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the first node in a line from the initial node, excluding whitespace.
|
||||
* @param {Object} context The node to check
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {ASTNode} the first node in the line
|
||||
*/
|
||||
function getFirstNodeInLine(context, node) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
let token = node;
|
||||
let lines;
|
||||
do {
|
||||
token = sourceCode.getTokenBefore(token);
|
||||
lines = token.type === 'JSXText' ?
|
||||
token.value.split('\n') :
|
||||
null;
|
||||
} while (
|
||||
token.type === 'JSXText' &&
|
||||
/^\s*$/.test(lines[lines.length - 1])
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is the first in its line, excluding whitespace.
|
||||
* @param {Object} context The node to check
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {Boolean} true if it's the first node in its line
|
||||
*/
|
||||
function isNodeFirstInLine(context, node) {
|
||||
const token = getFirstNodeInLine(context, node);
|
||||
const startLine = node.loc.start.line;
|
||||
const endLine = token ? token.loc.end.line : -1;
|
||||
return startLine !== endLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a function or arrow function expression.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {Boolean} true if it's a function-like expression
|
||||
*/
|
||||
function isFunctionLikeExpression(node) {
|
||||
return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a function.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {Boolean} true if it's a function
|
||||
*/
|
||||
function isFunction(node) {
|
||||
return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a class.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {Boolean} true if it's a class
|
||||
*/
|
||||
function isClass(node) {
|
||||
return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes quotes from around an identifier.
|
||||
* @param {string} string the identifier to strip
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripQuotes(string) {
|
||||
return string.replace(/^'|'$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the name of a key node
|
||||
* @param {Context} context The AST node with the key.
|
||||
* @param {ASTNode} node The AST node with the key.
|
||||
* @return {string} the name of the key
|
||||
*/
|
||||
function getKeyValue(context, node) {
|
||||
if (node.type === 'ObjectTypeProperty') {
|
||||
const tokens = context.getFirstTokens(node, 2);
|
||||
return (tokens[0].value === '+' || tokens[0].value === '-' ?
|
||||
tokens[1].value :
|
||||
stripQuotes(tokens[0].value)
|
||||
);
|
||||
}
|
||||
const key = node.key || node.argument;
|
||||
return key.type === 'Identifier' ? key.name : key.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is being assigned a value: props.bar = 'bar'
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isAssignmentLHS(node) {
|
||||
return (
|
||||
node.parent &&
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.left === node
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findReturnStatement,
|
||||
getFirstNodeInLine,
|
||||
getPropertyName,
|
||||
getPropertyNameNode,
|
||||
getComponentProperties,
|
||||
getKeyValue,
|
||||
isAssignmentLHS,
|
||||
isClass,
|
||||
isFunction,
|
||||
isFunctionLikeExpression,
|
||||
isNodeFirstInLine
|
||||
};
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @fileoverview Common defaultProps detection functionality.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fromEntries = require('object.fromentries');
|
||||
const astUtil = require('./ast');
|
||||
const propsUtil = require('./props');
|
||||
const variableUtil = require('./variable');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
|
||||
const QUOTES_REGEX = /^["']|["']$/g;
|
||||
|
||||
module.exports = function defaultPropsInstructions(context, components, utils) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
|
||||
* an Identifier, then the node is simply returned.
|
||||
* @param {ASTNode} node The node to resolve.
|
||||
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
|
||||
*/
|
||||
function resolveNodeValue(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return variableUtil.findVariableByName(context, node.name);
|
||||
}
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
propWrapperUtil.isPropWrapperFunction(context, node.callee.name) &&
|
||||
node.arguments && node.arguments[0]
|
||||
) {
|
||||
return resolveNodeValue(node.arguments[0]);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a DefaultProp from an ObjectExpression node.
|
||||
* @param {ASTNode} objectExpression ObjectExpression node.
|
||||
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
|
||||
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
|
||||
* from this ObjectExpression can't be resolved.
|
||||
*/
|
||||
function getDefaultPropsFromObjectExpression(objectExpression) {
|
||||
const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');
|
||||
|
||||
if (hasSpread) {
|
||||
return 'unresolved';
|
||||
}
|
||||
|
||||
return objectExpression.properties.map(defaultProp => ({
|
||||
name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''),
|
||||
node: defaultProp
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
|
||||
* marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
|
||||
* without risking false negatives.
|
||||
* @param {Object} component The component to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
function markDefaultPropsAsUnresolved(component) {
|
||||
components.set(component.node, {
|
||||
defaultProps: 'unresolved'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds defaultProps to the component passed in.
|
||||
* @param {ASTNode} component The component to add the defaultProps to.
|
||||
* @param {Object[]|'unresolved'} defaultProps defaultProps to add to the component or the string "unresolved"
|
||||
* if this component has defaultProps that can't be resolved.
|
||||
* @returns {void}
|
||||
*/
|
||||
function addDefaultPropsToComponent(component, defaultProps) {
|
||||
// Early return if this component's defaultProps is already marked as "unresolved".
|
||||
if (component.defaultProps === 'unresolved') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultProps === 'unresolved') {
|
||||
markDefaultPropsAsUnresolved(component);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaults = component.defaultProps || {};
|
||||
const newDefaultProps = Object.assign(
|
||||
{},
|
||||
defaults,
|
||||
fromEntries(defaultProps.map(prop => [prop.name, prop]))
|
||||
);
|
||||
|
||||
components.set(component.node, {
|
||||
defaultProps: newDefaultProps
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
|
||||
|
||||
if (!isDefaultProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find component this defaultProps belongs to
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g.:
|
||||
// MyComponent.propTypes = {
|
||||
// foo: React.PropTypes.string.isRequired,
|
||||
// bar: React.PropTypes.string
|
||||
// };
|
||||
//
|
||||
// or:
|
||||
//
|
||||
// MyComponent.propTypes = myPropTypes;
|
||||
if (node.parent.type === 'AssignmentExpression') {
|
||||
const expression = resolveNodeValue(node.parent.right);
|
||||
if (!expression || expression.type !== 'ObjectExpression') {
|
||||
// If a value can't be found, we mark the defaultProps declaration as "unresolved", because
|
||||
// we should ignore this component and not report any errors for it, to avoid false-positives
|
||||
// with e.g. external defaultProps declarations.
|
||||
if (isDefaultProp) {
|
||||
markDefaultPropsAsUnresolved(component);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g.:
|
||||
// MyComponent.propTypes.baz = React.PropTypes.string;
|
||||
if (node.parent.type === 'MemberExpression' && node.parent.parent &&
|
||||
node.parent.parent.type === 'AssignmentExpression') {
|
||||
addDefaultPropsToComponent(component, [{
|
||||
name: node.parent.property.name,
|
||||
node: node.parent.parent
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
// e.g.:
|
||||
// class Hello extends React.Component {
|
||||
// static get defaultProps() {
|
||||
// return {
|
||||
// name: 'Dean'
|
||||
// };
|
||||
// }
|
||||
// render() {
|
||||
// return <div>Hello {this.props.name}</div>;
|
||||
// }
|
||||
// }
|
||||
MethodDefinition(node) {
|
||||
if (!node.static || node.kind !== 'get') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propsUtil.isDefaultPropsDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find component this propTypes/defaultProps belongs to
|
||||
const component = components.get(utils.getParentES6Component());
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnStatement = utils.findReturnStatement(node);
|
||||
if (!returnStatement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expression = resolveNodeValue(returnStatement.argument);
|
||||
if (!expression || expression.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
||||
},
|
||||
|
||||
// e.g.:
|
||||
// class Greeting extends React.Component {
|
||||
// render() {
|
||||
// return (
|
||||
// <h1>Hello, {this.props.foo} {this.props.bar}</h1>
|
||||
// );
|
||||
// }
|
||||
// static defaultProps = {
|
||||
// foo: 'bar',
|
||||
// bar: 'baz'
|
||||
// };
|
||||
// }
|
||||
ClassProperty(node) {
|
||||
if (!(node.static && node.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propName = astUtil.getPropertyName(node);
|
||||
const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
|
||||
|
||||
if (!isDefaultProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find component this propTypes/defaultProps belongs to
|
||||
const component = components.get(utils.getParentES6Component());
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expression = resolveNodeValue(node.value);
|
||||
if (!expression || expression.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
|
||||
},
|
||||
|
||||
// e.g.:
|
||||
// React.createClass({
|
||||
// render: function() {
|
||||
// return <div>{this.props.foo}</div>;
|
||||
// },
|
||||
// getDefaultProps: function() {
|
||||
// return {
|
||||
// foo: 'default'
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
ObjectExpression(node) {
|
||||
// find component this propTypes/defaultProps belongs to
|
||||
const component = utils.isES5Component(node) && components.get(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the proptypes declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
|
||||
|
||||
if (isDefaultProp && property.value.type === 'FunctionExpression') {
|
||||
const returnStatement = utils.findReturnStatement(property);
|
||||
if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
function docsUrl(ruleName) {
|
||||
return `https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules/${ruleName}.md`;
|
||||
}
|
||||
|
||||
module.exports = docsUrl;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Logs out a message if there is no format option set.
|
||||
* @param {String} message - Message to log.
|
||||
*/
|
||||
function error(message) {
|
||||
if (!/=-(f|-format)=/.test(process.argv.join('='))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = error;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Find the token before the closing bracket.
|
||||
* @param {ASTNode} node - The JSX element node.
|
||||
* @returns {Token} The token before the closing bracket.
|
||||
*/
|
||||
function getTokenBeforeClosingBracket(node) {
|
||||
const attributes = node.attributes;
|
||||
if (attributes.length === 0) {
|
||||
return node.name;
|
||||
}
|
||||
return attributes[attributes.length - 1];
|
||||
}
|
||||
|
||||
module.exports = getTokenBeforeClosingBracket;
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for JSX
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
|
||||
const COMPAT_TAG_REGEX = /^[a-z]|-/;
|
||||
|
||||
/**
|
||||
* Checks if a node represents a DOM element.
|
||||
* @param {object} node - JSXOpeningElement to check.
|
||||
* @returns {boolean} Whether or not the node corresponds to a DOM element.
|
||||
*/
|
||||
function isDOMComponent(node) {
|
||||
let name = elementType(node);
|
||||
|
||||
// Get namespace if the type is JSXNamespacedName or JSXMemberExpression
|
||||
if (name.indexOf(':') > -1) {
|
||||
name = name.slice(0, name.indexOf(':'));
|
||||
} else if (name.indexOf('.') > -1) {
|
||||
name = name.slice(0, name.indexOf('.'));
|
||||
}
|
||||
|
||||
return COMPAT_TAG_REGEX.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a JSXElement is a fragment
|
||||
* @param {JSXElement} node
|
||||
* @param {string} reactPragma
|
||||
* @param {string} fragmentPragma
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isFragment(node, reactPragma, fragmentPragma) {
|
||||
const name = node.openingElement.name;
|
||||
|
||||
// <Fragment>
|
||||
if (name.type === 'JSXIdentifier' && name.name === fragmentPragma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// <React.Fragment>
|
||||
if (
|
||||
name.type === 'JSXMemberExpression' &&
|
||||
name.object.type === 'JSXIdentifier' &&
|
||||
name.object.name === reactPragma &&
|
||||
name.property.type === 'JSXIdentifier' &&
|
||||
name.property.name === fragmentPragma
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node represents a JSX element or fragment.
|
||||
* @param {object} node - node to check.
|
||||
* @returns {boolean} Whether or not the node if a JSX element or fragment.
|
||||
*/
|
||||
function isJSX(node) {
|
||||
return node && ['JSXElement', 'JSXFragment'].indexOf(node.type) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node is like `key={...}` as in `<Foo key={...} />`
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isJSXAttributeKey(node) {
|
||||
return node.type === 'JSXAttribute' &&
|
||||
node.name &&
|
||||
node.name.type === 'JSXIdentifier' &&
|
||||
node.name.name === 'key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value has only whitespaces
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isWhiteSpaces(value) {
|
||||
return typeof value === 'string' ? /^\s*$/.test(value) : false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isDOMComponent,
|
||||
isFragment,
|
||||
isJSX,
|
||||
isJSXAttributeKey,
|
||||
isWhiteSpaces
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for propWrapperFunctions setting
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** TODO: type {(string | { name: string, linkAttribute: string })[]} */
|
||||
/** @type {any} */
|
||||
const DEFAULT_LINK_COMPONENTS = ['a'];
|
||||
const DEFAULT_LINK_ATTRIBUTE = 'href';
|
||||
|
||||
function getLinkComponents(context) {
|
||||
const settings = context.settings || {};
|
||||
const linkComponents = /** @type {typeof DEFAULT_LINK_COMPONENTS} */ (
|
||||
DEFAULT_LINK_COMPONENTS.concat(settings.linkComponents || [])
|
||||
);
|
||||
return new Map(linkComponents.map((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return [value, DEFAULT_LINK_ATTRIBUTE];
|
||||
}
|
||||
return [value.name, value.linkAttribute];
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLinkComponents
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Logs out a message if there is no format option set.
|
||||
* @param {String} message - Message to log.
|
||||
*/
|
||||
function log(message) {
|
||||
if (!/=-(f|-format)=/.test(process.argv.join('='))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = log;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState in lifecycle methods
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('./docsUrl');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function mapTitle(methodName) {
|
||||
const map = {
|
||||
componentDidMount: 'did-mount',
|
||||
componentDidUpdate: 'did-update',
|
||||
componentWillUpdate: 'will-update'
|
||||
};
|
||||
const title = map[methodName];
|
||||
if (!title) {
|
||||
throw Error(`No docsUrl for '${methodName}'`);
|
||||
}
|
||||
return `no-${title}-set-state`;
|
||||
}
|
||||
|
||||
function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
|
||||
return {
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Prevent usage of setState in ${methodName}`,
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl(mapTitle(methodName))
|
||||
},
|
||||
|
||||
schema: [{
|
||||
enum: ['disallow-in-func']
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const mode = context.options[0] || 'allow-in-func';
|
||||
|
||||
function nameMatches(name) {
|
||||
if (name === methodName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof shouldCheckUnsafeCb === 'function' && shouldCheckUnsafeCb(context)) {
|
||||
return name === `UNSAFE_${methodName}`;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.object.type !== 'ThisExpression' ||
|
||||
callee.property.name !== 'setState'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const ancestors = context.getAncestors(callee).reverse();
|
||||
let depth = 0;
|
||||
ancestors.some((ancestor) => {
|
||||
if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
|
||||
depth++;
|
||||
}
|
||||
if (
|
||||
(ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty') ||
|
||||
!nameMatches(ancestor.key.name) ||
|
||||
(mode !== 'disallow-in-func' && depth > 1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
context.report({
|
||||
node: callee,
|
||||
message: `Do not use setState in ${ancestor.key.name}`
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = makeNoMethodSetStateRule;
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for React pragma configuration
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const JSX_ANNOTATION_REGEX = /^\*\s*@jsx\s+([^\s]+)/;
|
||||
// Does not check for reserved keywords or unicode characters
|
||||
const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;
|
||||
|
||||
|
||||
function getCreateClassFromContext(context) {
|
||||
let pragma = 'createReactClass';
|
||||
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
|
||||
if (context.settings.react && context.settings.react.createClass) {
|
||||
pragma = context.settings.react.createClass;
|
||||
}
|
||||
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
|
||||
throw new Error(`createClass pragma ${pragma} is not a valid function name`);
|
||||
}
|
||||
return pragma;
|
||||
}
|
||||
|
||||
function getFragmentFromContext(context) {
|
||||
let pragma = 'Fragment';
|
||||
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
|
||||
if (context.settings.react && context.settings.react.fragment) {
|
||||
pragma = context.settings.react.fragment;
|
||||
}
|
||||
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
|
||||
throw new Error(`Fragment pragma ${pragma} is not a valid identifier`);
|
||||
}
|
||||
return pragma;
|
||||
}
|
||||
|
||||
function getFromContext(context) {
|
||||
let pragma = 'React';
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
const pragmaNode = sourceCode.getAllComments().find(node => JSX_ANNOTATION_REGEX.test(node.value));
|
||||
|
||||
if (pragmaNode) {
|
||||
const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value);
|
||||
pragma = matches[1].split('.')[0];
|
||||
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
|
||||
} else if (context.settings.react && context.settings.react.pragma) {
|
||||
pragma = context.settings.react.pragma;
|
||||
}
|
||||
|
||||
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
|
||||
throw new Error(`React pragma ${pragma} is not a valid identifier`);
|
||||
}
|
||||
return pragma;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCreateClassFromContext,
|
||||
getFragmentFromContext,
|
||||
getFromContext
|
||||
};
|
||||
+747
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* @fileoverview Common propTypes detection functionality.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const annotations = require('./annotations');
|
||||
const propsUtil = require('./props');
|
||||
const variableUtil = require('./variable');
|
||||
const versionUtil = require('./version');
|
||||
const propWrapperUtil = require('./propWrapper');
|
||||
const getKeyValue = require('./ast').getKeyValue;
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a props as a generic type in a flow-annotated class.
|
||||
*
|
||||
* @param {ASTNode} node the AST node being checked.
|
||||
* @returns {Boolean} True if the node is a class with generic prop types, false if not.
|
||||
*/
|
||||
function isSuperTypeParameterPropsDeclaration(node) {
|
||||
if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
|
||||
if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through a properties node, like a customized forEach.
|
||||
* @param {Object} context Array of properties to iterate.
|
||||
* @param {Object[]} properties Array of properties to iterate.
|
||||
* @param {Function} fn Function to call on each property, receives property key
|
||||
and property value. (key, value) => void
|
||||
*/
|
||||
function iterateProperties(context, properties, fn) {
|
||||
if (properties && properties.length && typeof fn === 'function') {
|
||||
for (let i = 0, j = properties.length; i < j; i++) {
|
||||
const node = properties[i];
|
||||
const key = getKeyValue(context, node);
|
||||
|
||||
const value = node.value;
|
||||
fn(key, value, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is inside a class body.
|
||||
*
|
||||
* @param {ASTNode} node the AST node being checked.
|
||||
* @returns {Boolean} True if the node has a ClassBody ancestor, false if not.
|
||||
*/
|
||||
function isInsideClassBody(node) {
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
if (parent.type === 'ClassBody') {
|
||||
return true;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = function propTypesInstructions(context, components, utils) {
|
||||
// Used to track the type annotations in scope.
|
||||
// Necessary because babel's scopes do not track type annotations.
|
||||
let stack = null;
|
||||
|
||||
const classExpressions = [];
|
||||
const defaults = {customValidators: []};
|
||||
const configuration = Object.assign({}, defaults, context.options[0] || {});
|
||||
const customValidators = configuration.customValidators;
|
||||
|
||||
/**
|
||||
* Returns the full scope.
|
||||
* @returns {Object} The whole scope.
|
||||
*/
|
||||
function typeScope() {
|
||||
return stack[stack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node from the scope.
|
||||
* @param {string} key The name of the identifier to access.
|
||||
* @returns {ASTNode} The ASTNode associated with the given identifier.
|
||||
*/
|
||||
function getInTypeScope(key) {
|
||||
return stack[stack.length - 1][key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the new value in the scope.
|
||||
* @param {string} key The name of the identifier to access
|
||||
* @param {ASTNode} value The new value for the identifier.
|
||||
* @returns {ASTNode} The ASTNode associated with the given identifier.
|
||||
*/
|
||||
function setInTypeScope(key, value) {
|
||||
stack[stack.length - 1][key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if prop should be validated by plugin-react-proptypes
|
||||
* @param {String} validator Name of validator to check.
|
||||
* @returns {Boolean} True if validator should be checked by custom validator.
|
||||
*/
|
||||
function hasCustomValidator(validator) {
|
||||
return customValidators.indexOf(validator) !== -1;
|
||||
}
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
/** @type {TypeDeclarationBuilders} */
|
||||
const typeDeclarationBuilders = {
|
||||
GenericTypeAnnotation(annotation, parentName, seen) {
|
||||
if (getInTypeScope(annotation.id.name)) {
|
||||
return buildTypeAnnotationDeclarationTypes(getInTypeScope(annotation.id.name), parentName, seen);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
ObjectTypeAnnotation(annotation, parentName, seen) {
|
||||
let containsObjectTypeSpread = false;
|
||||
const containsIndexers = Boolean(annotation.indexers && annotation.indexers.length);
|
||||
const shapeTypeDefinition = {
|
||||
type: 'shape',
|
||||
children: {}
|
||||
};
|
||||
iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => {
|
||||
const fullName = [parentName, childKey].join('.');
|
||||
if (!childKey && !childValue) {
|
||||
containsObjectTypeSpread = true;
|
||||
} else {
|
||||
const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
|
||||
types.fullName = fullName;
|
||||
types.name = childKey;
|
||||
types.node = propNode;
|
||||
types.isRequired = !childValue.optional;
|
||||
shapeTypeDefinition.children[childKey] = types;
|
||||
}
|
||||
});
|
||||
|
||||
// Mark if this shape has spread or an indexer. We will know to consider all props from this shape as having propTypes,
|
||||
// but still have the ability to detect unused children of this shape.
|
||||
shapeTypeDefinition.containsSpread = containsObjectTypeSpread;
|
||||
shapeTypeDefinition.containsIndexers = containsIndexers;
|
||||
|
||||
return shapeTypeDefinition;
|
||||
},
|
||||
|
||||
UnionTypeAnnotation(annotation, parentName, seen) {
|
||||
/** @type {UnionTypeDefinition} */
|
||||
const unionTypeDefinition = {
|
||||
type: 'union',
|
||||
children: []
|
||||
};
|
||||
for (let i = 0, j = annotation.types.length; i < j; i++) {
|
||||
const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName, seen);
|
||||
// keep only complex type
|
||||
if (type.type) {
|
||||
if (type.children === true) {
|
||||
// every child is accepted for one type, abort type analysis
|
||||
unionTypeDefinition.children = true;
|
||||
return unionTypeDefinition;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type);
|
||||
}
|
||||
if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).length === 0) {
|
||||
// no complex type found, simply accept everything
|
||||
return {};
|
||||
}
|
||||
return unionTypeDefinition;
|
||||
},
|
||||
|
||||
ArrayTypeAnnotation(annotation, parentName, seen) {
|
||||
const fullName = [parentName, '*'].join('.');
|
||||
const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName, seen);
|
||||
child.fullName = fullName;
|
||||
child.name = '__ANY_KEY__';
|
||||
child.node = annotation;
|
||||
return {
|
||||
type: 'object',
|
||||
children: {
|
||||
__ANY_KEY__: child
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
/**
|
||||
* Resolve the type annotation for a given node.
|
||||
* Flow annotations are sometimes wrapped in outer `TypeAnnotation`
|
||||
* and `NullableTypeAnnotation` nodes which obscure the annotation we're
|
||||
* interested in.
|
||||
* This method also resolves type aliases where possible.
|
||||
*
|
||||
* @param {ASTNode} node The annotation or a node containing the type annotation.
|
||||
* @returns {ASTNode} The resolved type annotation for the node.
|
||||
*/
|
||||
function resolveTypeAnnotation(node) {
|
||||
let annotation = (node.left && node.left.typeAnnotation) || node.typeAnnotation || node;
|
||||
while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
|
||||
annotation = annotation.typeAnnotation;
|
||||
}
|
||||
if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
|
||||
return getInTypeScope(annotation.id.name);
|
||||
}
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the representation of the React props type annotation for the component.
|
||||
* The representation is used to verify nested used properties.
|
||||
* @param {ASTNode} annotation Type annotation for the props class property.
|
||||
* @param {String} parentName
|
||||
* @param {Set<ASTNode>} [seen]
|
||||
* @return {Object} The representation of the declaration, empty object means
|
||||
* the property is declared without the need for further analysis.
|
||||
*/
|
||||
function buildTypeAnnotationDeclarationTypes(annotation, parentName, seen) {
|
||||
if (typeof seen === 'undefined') {
|
||||
// Keeps track of annotations we've already seen to
|
||||
// prevent problems with recursive types.
|
||||
seen = new Set();
|
||||
}
|
||||
if (seen.has(annotation)) {
|
||||
// This must be a recursive type annotation, so just accept anything.
|
||||
return {};
|
||||
}
|
||||
seen.add(annotation);
|
||||
|
||||
if (annotation.type in typeDeclarationBuilders) {
|
||||
return typeDeclarationBuilders[annotation.type](annotation, parentName, seen);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all props found inside ObjectTypeAnnotaiton as declared.
|
||||
*
|
||||
* Modifies the declaredProperties object
|
||||
* @param {ASTNode} propTypes
|
||||
* @param {Object} declaredPropTypes
|
||||
* @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
|
||||
*/
|
||||
function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
|
||||
let ignorePropsValidation = false;
|
||||
|
||||
iterateProperties(context, propTypes.properties, (key, value, propNode) => {
|
||||
if (!value) {
|
||||
ignorePropsValidation = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const types = buildTypeAnnotationDeclarationTypes(value, key);
|
||||
types.fullName = key;
|
||||
types.name = key;
|
||||
types.node = propNode;
|
||||
types.isRequired = !propNode.optional;
|
||||
declaredPropTypes[key] = types;
|
||||
});
|
||||
|
||||
return ignorePropsValidation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all props found inside IntersectionTypeAnnotation as declared.
|
||||
* Since InterSectionTypeAnnotations can be nested, this handles recursively.
|
||||
*
|
||||
* Modifies the declaredPropTypes object
|
||||
* @param {ASTNode} propTypes
|
||||
* @param {Object} declaredPropTypes
|
||||
* @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
|
||||
*/
|
||||
function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) {
|
||||
return propTypes.types.some((annotation) => {
|
||||
if (annotation.type === 'ObjectTypeAnnotation') {
|
||||
return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes);
|
||||
}
|
||||
|
||||
if (annotation.type === 'UnionTypeAnnotation') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Type can't be resolved
|
||||
if (!annotation.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const typeNode = getInTypeScope(annotation.id.name);
|
||||
|
||||
if (!typeNode) {
|
||||
return true;
|
||||
}
|
||||
if (typeNode.type === 'IntersectionTypeAnnotation') {
|
||||
return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
|
||||
}
|
||||
|
||||
return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the representation of the React propTypes for the component.
|
||||
* The representation is used to verify nested used properties.
|
||||
* @param {ASTNode} value Node of the PropTypes for the desired property
|
||||
* @param {string} parentName
|
||||
* @return {Object} The representation of the declaration, empty object means
|
||||
* the property is declared without the need for further analysis.
|
||||
*/
|
||||
function buildReactDeclarationTypes(value, parentName) {
|
||||
if (
|
||||
value &&
|
||||
value.callee &&
|
||||
value.callee.object &&
|
||||
hasCustomValidator(value.callee.object.name)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (
|
||||
value &&
|
||||
value.type === 'MemberExpression' &&
|
||||
value.property &&
|
||||
value.property.name &&
|
||||
value.property.name === 'isRequired'
|
||||
) {
|
||||
value = value.object;
|
||||
}
|
||||
|
||||
// Verify PropTypes that are functions
|
||||
if (
|
||||
value &&
|
||||
value.type === 'CallExpression' &&
|
||||
value.callee &&
|
||||
value.callee.property &&
|
||||
value.callee.property.name &&
|
||||
value.arguments &&
|
||||
value.arguments.length > 0
|
||||
) {
|
||||
const callName = value.callee.property.name;
|
||||
const argument = value.arguments[0];
|
||||
switch (callName) {
|
||||
case 'shape': {
|
||||
if (argument.type !== 'ObjectExpression') {
|
||||
// Invalid proptype or cannot analyse statically
|
||||
return {};
|
||||
}
|
||||
const shapeTypeDefinition = {
|
||||
type: 'shape',
|
||||
children: {}
|
||||
};
|
||||
iterateProperties(context, argument.properties, (childKey, childValue, propNode) => {
|
||||
if (childValue) { // skip spread propTypes
|
||||
const fullName = [parentName, childKey].join('.');
|
||||
const types = buildReactDeclarationTypes(childValue, fullName);
|
||||
types.fullName = fullName;
|
||||
types.name = childKey;
|
||||
types.node = propNode;
|
||||
shapeTypeDefinition.children[childKey] = types;
|
||||
}
|
||||
});
|
||||
return shapeTypeDefinition;
|
||||
}
|
||||
case 'arrayOf':
|
||||
case 'objectOf': {
|
||||
const fullName = [parentName, '*'].join('.');
|
||||
const child = buildReactDeclarationTypes(argument, fullName);
|
||||
child.fullName = fullName;
|
||||
child.name = '__ANY_KEY__';
|
||||
child.node = argument;
|
||||
return {
|
||||
type: 'object',
|
||||
children: {
|
||||
__ANY_KEY__: child
|
||||
}
|
||||
};
|
||||
}
|
||||
case 'oneOfType': {
|
||||
if (
|
||||
!argument.elements ||
|
||||
!argument.elements.length
|
||||
) {
|
||||
// Invalid proptype or cannot analyse statically
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @type {UnionTypeDefinition} */
|
||||
const unionTypeDefinition = {
|
||||
type: 'union',
|
||||
children: []
|
||||
};
|
||||
for (let i = 0, j = argument.elements.length; i < j; i++) {
|
||||
const type = buildReactDeclarationTypes(argument.elements[i], parentName);
|
||||
// keep only complex type
|
||||
if (type.type) {
|
||||
if (type.children === true) {
|
||||
// every child is accepted for one type, abort type analysis
|
||||
unionTypeDefinition.children = true;
|
||||
return unionTypeDefinition;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type);
|
||||
}
|
||||
if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).length === 0) {
|
||||
// no complex type found, simply accept everything
|
||||
return {};
|
||||
}
|
||||
return unionTypeDefinition;
|
||||
}
|
||||
case 'instanceOf':
|
||||
return {
|
||||
type: 'instance',
|
||||
// Accept all children because we can't know what type they are
|
||||
children: true
|
||||
};
|
||||
case 'oneOf':
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
// Unknown property or accepts everything (any, object, ...)
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mark a prop type as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {ASTNode} propTypes The AST node containing the proptypes
|
||||
*/
|
||||
function markPropTypesAsDeclared(node, propTypes) {
|
||||
let componentNode = node;
|
||||
while (componentNode && !components.get(componentNode)) {
|
||||
componentNode = componentNode.parent;
|
||||
}
|
||||
const component = components.get(componentNode);
|
||||
const declaredPropTypes = component && component.declaredPropTypes || {};
|
||||
let ignorePropsValidation = component && component.ignorePropsValidation || false;
|
||||
switch (propTypes && propTypes.type) {
|
||||
case 'ObjectTypeAnnotation':
|
||||
ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
|
||||
break;
|
||||
case 'ObjectExpression':
|
||||
iterateProperties(context, propTypes.properties, (key, value, propNode) => {
|
||||
if (!value) {
|
||||
ignorePropsValidation = true;
|
||||
return;
|
||||
}
|
||||
const types = buildReactDeclarationTypes(value, key);
|
||||
types.fullName = key;
|
||||
types.name = key;
|
||||
types.node = propNode;
|
||||
types.isRequired = propsUtil.isRequiredPropType(value);
|
||||
declaredPropTypes[key] = types;
|
||||
});
|
||||
break;
|
||||
case 'MemberExpression': {
|
||||
let curDeclaredPropTypes = declaredPropTypes;
|
||||
// Walk the list of properties, until we reach the assignment
|
||||
// ie: ClassX.propTypes.a.b.c = ...
|
||||
while (
|
||||
propTypes &&
|
||||
propTypes.parent &&
|
||||
propTypes.parent.type !== 'AssignmentExpression' &&
|
||||
propTypes.property &&
|
||||
curDeclaredPropTypes
|
||||
) {
|
||||
const propName = propTypes.property.name;
|
||||
if (propName in curDeclaredPropTypes) {
|
||||
curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
|
||||
propTypes = propTypes.parent;
|
||||
} else {
|
||||
// This will crash at runtime because we haven't seen this key before
|
||||
// stop this and do not declare it
|
||||
propTypes = null;
|
||||
}
|
||||
}
|
||||
if (propTypes && propTypes.parent && propTypes.property) {
|
||||
if (!(propTypes === propTypes.parent.left && propTypes.parent.left.object)) {
|
||||
ignorePropsValidation = true;
|
||||
break;
|
||||
}
|
||||
const parentProp = context.getSource(propTypes.parent.left.object).replace(/^.*\.propTypes\./, '');
|
||||
const types = buildReactDeclarationTypes(
|
||||
propTypes.parent.right,
|
||||
parentProp
|
||||
);
|
||||
|
||||
types.name = propTypes.property.name;
|
||||
types.fullName = [parentProp, propTypes.property.name].join('.');
|
||||
types.node = propTypes.parent;
|
||||
types.isRequired = propsUtil.isRequiredPropType(propTypes.parent.right);
|
||||
curDeclaredPropTypes[propTypes.property.name] = types;
|
||||
} else {
|
||||
let isUsedInPropTypes = false;
|
||||
let n = propTypes;
|
||||
while (n) {
|
||||
if (n.type === 'AssignmentExpression' && propsUtil.isPropTypesDeclaration(n.left) ||
|
||||
(n.type === 'ClassProperty' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n)) {
|
||||
// Found a propType used inside of another propType. This is not considered usage, we'll still validate
|
||||
// this component.
|
||||
isUsedInPropTypes = true;
|
||||
break;
|
||||
}
|
||||
n = n.parent;
|
||||
}
|
||||
if (!isUsedInPropTypes) {
|
||||
ignorePropsValidation = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Identifier': {
|
||||
const variablesInScope = variableUtil.variablesInScope(context);
|
||||
const firstMatchingVariable = variablesInScope
|
||||
.find(variableInScope => variableInScope.name === propTypes.name);
|
||||
if (firstMatchingVariable) {
|
||||
const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1];
|
||||
markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
|
||||
return;
|
||||
}
|
||||
ignorePropsValidation = true;
|
||||
break;
|
||||
}
|
||||
case 'CallExpression': {
|
||||
if (
|
||||
propWrapperUtil.isPropWrapperFunction(
|
||||
context,
|
||||
context.getSourceCode().getText(propTypes.callee)
|
||||
) &&
|
||||
propTypes.arguments && propTypes.arguments[0]
|
||||
) {
|
||||
markPropTypesAsDeclared(node, propTypes.arguments[0]);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'IntersectionTypeAnnotation':
|
||||
ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
|
||||
break;
|
||||
case 'GenericTypeAnnotation':
|
||||
if (propTypes.id.name === '$ReadOnly') {
|
||||
ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(
|
||||
propTypes.typeParameters.params[0],
|
||||
declaredPropTypes
|
||||
);
|
||||
} else {
|
||||
ignorePropsValidation = true;
|
||||
}
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
ignorePropsValidation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
components.set(node, {
|
||||
declaredPropTypes,
|
||||
ignorePropsValidation
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function markAnnotatedFunctionArgumentsAsDeclared(node) {
|
||||
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInsideClassBody(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const param = node.params[0];
|
||||
if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') {
|
||||
param.typeAnnotation.typeAnnotation.types.forEach((annotation) => {
|
||||
if (annotation.type === 'GenericTypeAnnotation') {
|
||||
markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation));
|
||||
} else {
|
||||
markPropTypesAsDeclared(node, annotation);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the type annotation for a given class declaration node with superTypeParameters.
|
||||
*
|
||||
* @param {ASTNode} node The annotation or a node containing the type annotation.
|
||||
* @returns {ASTNode} The resolved type annotation for the node.
|
||||
*/
|
||||
function resolveSuperParameterPropsType(node) {
|
||||
let propsParameterPosition;
|
||||
try {
|
||||
// Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
|
||||
// Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
|
||||
propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
|
||||
} catch (e) {
|
||||
// In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
|
||||
propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
|
||||
}
|
||||
|
||||
let annotation = node.superTypeParameters.params[propsParameterPosition];
|
||||
while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
|
||||
annotation = annotation.typeAnnotation;
|
||||
}
|
||||
|
||||
if (annotation && annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
|
||||
return getInTypeScope(annotation.id.name);
|
||||
}
|
||||
return annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a `props` class property with a flow type annotation.
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
|
||||
*/
|
||||
function isAnnotatedClassPropsDeclaration(node) {
|
||||
if (node && node.type === 'ClassProperty') {
|
||||
const tokens = context.getFirstTokens(node, 2);
|
||||
if (
|
||||
node.typeAnnotation && (
|
||||
tokens[0].value === 'props' ||
|
||||
(tokens[1] && tokens[1].value === 'props')
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
ClassExpression(node) {
|
||||
// TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
|
||||
// This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
|
||||
// processing class expressions until when the program exists.
|
||||
classExpressions.push(node);
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
if (isSuperTypeParameterPropsDeclaration(node)) {
|
||||
markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
|
||||
}
|
||||
},
|
||||
|
||||
ClassProperty(node) {
|
||||
if (isAnnotatedClassPropsDeclaration(node)) {
|
||||
markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
|
||||
} else if (propsUtil.isPropTypesDeclaration(node)) {
|
||||
markPropTypesAsDeclared(node, node.value);
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
// Search for the proptypes declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (!propsUtil.isPropTypesDeclaration(property)) {
|
||||
return;
|
||||
}
|
||||
markPropTypesAsDeclared(node, property.value);
|
||||
});
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
if (node.parent.type !== 'MethodDefinition') {
|
||||
markAnnotatedFunctionArgumentsAsDeclared(node);
|
||||
}
|
||||
},
|
||||
|
||||
FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
|
||||
|
||||
ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,
|
||||
|
||||
MemberExpression(node) {
|
||||
if (propsUtil.isPropTypesDeclaration(node)) {
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i = node.value.body.body.length - 1;
|
||||
for (; i >= 0; i--) {
|
||||
if (node.value.body.body[i].type === 'ReturnStatement') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= 0) {
|
||||
markPropTypesAsDeclared(node, node.value.body.body[i].argument);
|
||||
}
|
||||
},
|
||||
|
||||
TypeAlias(node) {
|
||||
setInTypeScope(node.id.name, node.right);
|
||||
},
|
||||
|
||||
TypeParameterDeclaration(node) {
|
||||
const identifier = node.params[0];
|
||||
|
||||
if (identifier.typeAnnotation) {
|
||||
setInTypeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
|
||||
}
|
||||
},
|
||||
|
||||
Program() {
|
||||
stack = [{}];
|
||||
},
|
||||
|
||||
BlockStatement() {
|
||||
stack.push(Object.create(typeScope()));
|
||||
},
|
||||
|
||||
'BlockStatement:exit': function () {
|
||||
stack.pop();
|
||||
},
|
||||
|
||||
'Program:exit': function () {
|
||||
classExpressions.forEach((node) => {
|
||||
if (isSuperTypeParameterPropsDeclaration(node)) {
|
||||
markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @fileoverview Common propTypes sorting functionality.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('./ast');
|
||||
|
||||
/**
|
||||
* Returns the value name of a node.
|
||||
*
|
||||
* @param {ASTNode} node the node to check.
|
||||
* @returns {String} The name of the node.
|
||||
*/
|
||||
function getValueName(node) {
|
||||
return node.type === 'Property' && node.value.property && node.value.property.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prop is required or not.
|
||||
*
|
||||
* @param {ASTNode} node the prop to check.
|
||||
* @returns {Boolean} true if the prop is required.
|
||||
*/
|
||||
function isRequiredProp(node) {
|
||||
return getValueName(node) === 'isRequired';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the proptype is a callback by checking if it starts with 'on'.
|
||||
*
|
||||
* @param {String} propName the name of the proptype to check.
|
||||
* @returns {Boolean} true if the proptype is a callback.
|
||||
*/
|
||||
function isCallbackPropName(propName) {
|
||||
return /^on[A-Z]/.test(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prop is PropTypes.shape.
|
||||
*
|
||||
* @param {ASTNode} node the prop to check.
|
||||
* @returns {Boolean} true if the prop is PropTypes.shape.
|
||||
*/
|
||||
function isShapeProp(node) {
|
||||
return Boolean(
|
||||
node && node.callee && node.callee.property && node.callee.property.name === 'shape'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties of a PropTypes.shape.
|
||||
*
|
||||
* @param {ASTNode} node the prop to check.
|
||||
* @returns {Array} the properties of the PropTypes.shape node.
|
||||
*/
|
||||
function getShapeProperties(node) {
|
||||
return node.arguments && node.arguments[0] && node.arguments[0].properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two elements.
|
||||
*
|
||||
* @param {ASTNode} a the first element to compare.
|
||||
* @param {ASTNode} b the second element to compare.
|
||||
* @param {Context} context The context of the two nodes.
|
||||
* @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements.
|
||||
* @param {Boolean=} requiredFirst whether or not to sort required elements first.
|
||||
* @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else.
|
||||
* @returns {Number} the sort order of the two elements.
|
||||
*/
|
||||
function sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast) {
|
||||
const aKey = String(astUtil.getKeyValue(context, a));
|
||||
const bKey = String(astUtil.getKeyValue(context, b));
|
||||
|
||||
if (requiredFirst) {
|
||||
if (isRequiredProp(a) && !isRequiredProp(b)) {
|
||||
return -1;
|
||||
}
|
||||
if (!isRequiredProp(a) && isRequiredProp(b)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (callbacksLast) {
|
||||
if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) {
|
||||
return 1;
|
||||
}
|
||||
if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
return aKey.localeCompare(bKey);
|
||||
}
|
||||
|
||||
if (aKey < bKey) {
|
||||
return -1;
|
||||
}
|
||||
if (aKey > bKey) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes sort order of prop types.
|
||||
*
|
||||
* @param {Fixer} fixer the first element to compare.
|
||||
* @param {Object} context the second element to compare.
|
||||
* @param {Array} declarations The context of the two nodes.
|
||||
* @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements.
|
||||
* @param {Boolean=} requiredFirst whether or not to sort required elements first.
|
||||
* @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else.
|
||||
* @param {Boolean=} sortShapeProp whether or not to sort propTypes defined in PropTypes.shape.
|
||||
* @returns {Object|*|{range, text}} the sort order of the two elements.
|
||||
*/
|
||||
function fixPropTypesSort(fixer, context, declarations, ignoreCase, requiredFirst, callbacksLast, sortShapeProp) {
|
||||
function sortInSource(allNodes, source) {
|
||||
const originalSource = source;
|
||||
const nodeGroups = allNodes.reduce((acc, curr) => {
|
||||
if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
|
||||
acc.push([]);
|
||||
} else {
|
||||
acc[acc.length - 1].push(curr);
|
||||
}
|
||||
return acc;
|
||||
}, [[]]);
|
||||
|
||||
nodeGroups.forEach((nodes) => {
|
||||
const sortedAttributes = nodes
|
||||
.slice()
|
||||
.sort((a, b) => sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast));
|
||||
|
||||
source = nodes.reduceRight((acc, attr, index) => {
|
||||
const sortedAttr = sortedAttributes[index];
|
||||
let sortedAttrText = context.getSourceCode().getText(sortedAttr);
|
||||
if (sortShapeProp && isShapeProp(sortedAttr.value)) {
|
||||
const shape = getShapeProperties(sortedAttr.value);
|
||||
if (shape) {
|
||||
const attrSource = sortInSource(
|
||||
shape,
|
||||
originalSource
|
||||
);
|
||||
sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]);
|
||||
}
|
||||
}
|
||||
return `${acc.slice(0, attr.range[0])}${sortedAttrText}${acc.slice(attr.range[1])}`;
|
||||
}, source);
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
const source = sortInSource(declarations, context.getSourceCode().getText());
|
||||
|
||||
const rangeStart = declarations[0].range[0];
|
||||
const rangeEnd = declarations[declarations.length - 1].range[1];
|
||||
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fixPropTypesSort
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for propWrapperFunctions setting
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function getPropWrapperFunctions(context) {
|
||||
return new Set(context.settings.propWrapperFunctions || []);
|
||||
}
|
||||
|
||||
function isPropWrapperFunction(context, name) {
|
||||
if (typeof name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const propWrapperFunctions = getPropWrapperFunctions(context);
|
||||
const splitName = name.split('.');
|
||||
return Array.from(propWrapperFunctions).some((func) => {
|
||||
if (splitName.length === 2 && func.object === splitName[0] && func.property === splitName[1]) {
|
||||
return true;
|
||||
}
|
||||
return name === func || func.property === name;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPropWrapperFunctions,
|
||||
isPropWrapperFunction
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for props
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('./ast');
|
||||
|
||||
/**
|
||||
* Checks if the Identifier node passed in looks like a propTypes declaration.
|
||||
* @param {ASTNode} node The node to check. Must be an Identifier node.
|
||||
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
|
||||
*/
|
||||
function isPropTypesDeclaration(node) {
|
||||
if (node && node.type === 'ClassProperty') {
|
||||
// Flow support
|
||||
if (node.typeAnnotation && node.key.name === 'props') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return astUtil.getPropertyName(node) === 'propTypes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node passed in looks like a contextTypes declaration.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {Boolean} `true` if the node is a contextTypes declaration, `false` if not
|
||||
*/
|
||||
function isContextTypesDeclaration(node) {
|
||||
if (node && node.type === 'ClassProperty') {
|
||||
// Flow support
|
||||
if (node.typeAnnotation && node.key.name === 'context') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return astUtil.getPropertyName(node) === 'contextTypes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node passed in looks like a contextType declaration.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {Boolean} `true` if the node is a contextType declaration, `false` if not
|
||||
*/
|
||||
function isContextTypeDeclaration(node) {
|
||||
return astUtil.getPropertyName(node) === 'contextType';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node passed in looks like a childContextTypes declaration.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {Boolean} `true` if the node is a childContextTypes declaration, `false` if not
|
||||
*/
|
||||
function isChildContextTypesDeclaration(node) {
|
||||
return astUtil.getPropertyName(node) === 'childContextTypes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Identifier node passed in looks like a defaultProps declaration.
|
||||
* @param {ASTNode} node The node to check. Must be an Identifier node.
|
||||
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
|
||||
*/
|
||||
function isDefaultPropsDeclaration(node) {
|
||||
const propName = astUtil.getPropertyName(node);
|
||||
return (propName === 'defaultProps' || propName === 'getDefaultProps');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a display name
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Boolean} True if we are declaring a display name, false if not.
|
||||
*/
|
||||
function isDisplayNameDeclaration(node) {
|
||||
switch (node.type) {
|
||||
case 'ClassProperty':
|
||||
return node.key && node.key.name === 'displayName';
|
||||
case 'Identifier':
|
||||
return node.name === 'displayName';
|
||||
case 'Literal':
|
||||
return node.value === 'displayName';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
|
||||
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
|
||||
* @returns {Boolean} `true` if this PropType is required, `false` if not.
|
||||
*/
|
||||
function isRequiredPropType(propTypeExpression) {
|
||||
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isPropTypesDeclaration,
|
||||
isContextTypesDeclaration,
|
||||
isContextTypeDeclaration,
|
||||
isChildContextTypesDeclaration,
|
||||
isDefaultPropsDeclaration,
|
||||
isDisplayNameDeclaration,
|
||||
isRequiredPropType
|
||||
};
|
||||
+538
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* @fileoverview Common used propTypes detection functionality.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('./ast');
|
||||
const versionUtil = require('./version');
|
||||
const ast = require('./ast');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate'];
|
||||
const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate'];
|
||||
|
||||
function createPropVariables() {
|
||||
/** @type {Map<string, string[]>} Maps the variable to its definition. `props.a.b` is stored as `['a', 'b']` */
|
||||
let propVariables = new Map();
|
||||
let hasBeenWritten = false;
|
||||
const stack = [{propVariables, hasBeenWritten}];
|
||||
return {
|
||||
pushScope() {
|
||||
// popVariables is not copied until first write.
|
||||
stack.push({propVariables, hasBeenWritten: false});
|
||||
},
|
||||
popScope() {
|
||||
stack.pop();
|
||||
propVariables = stack[stack.length - 1].propVariables;
|
||||
hasBeenWritten = stack[stack.length - 1].hasBeenWritten;
|
||||
},
|
||||
/**
|
||||
* Add a variable name to the current scope
|
||||
* @param {string} name
|
||||
* @param {string[]} allNames Example: `props.a.b` should be formatted as `['a', 'b']`
|
||||
* @returns {Map<string, string[]>}
|
||||
*/
|
||||
set(name, allNames) {
|
||||
if (!hasBeenWritten) {
|
||||
// copy on write
|
||||
propVariables = new Map(propVariables);
|
||||
Object.assign(stack[stack.length - 1], {propVariables, hasBeenWritten: true});
|
||||
stack[stack.length - 1].hasBeenWritten = true;
|
||||
}
|
||||
return propVariables.set(name, allNames);
|
||||
},
|
||||
/**
|
||||
* Get the definition of a variable.
|
||||
* @param {string} name
|
||||
* @returns {string[]} Example: `props.a.b` is represented by `['a', 'b']`
|
||||
*/
|
||||
get(name) {
|
||||
return propVariables.get(name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the string is one of `props`, `nextProps`, or `prevProps`
|
||||
* @param {string} name The AST node being checked.
|
||||
* @returns {Boolean} True if the prop name matches
|
||||
*/
|
||||
function isCommonVariableNameForProps(name) {
|
||||
return name === 'props' || name === 'nextProps' || name === 'prevProps';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the component must be validated
|
||||
* @param {Object} component The component to process
|
||||
* @returns {Boolean} True if the component must be validated, false if not.
|
||||
*/
|
||||
function mustBeValidated(component) {
|
||||
return !!(component && !component.ignorePropsValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are in a lifecycle method
|
||||
* @param {object} context
|
||||
* @param {boolean} checkAsyncSafeLifeCycles
|
||||
* @return {boolean} true if we are in a class constructor, false if not
|
||||
*/
|
||||
function inLifeCycleMethod(context, checkAsyncSafeLifeCycles) {
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
if (scope.block && scope.block.parent && scope.block.parent.key) {
|
||||
const name = scope.block.parent.key.name;
|
||||
|
||||
if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given node is a React Component lifecycle method
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {boolean} checkAsyncSafeLifeCycles
|
||||
* @return {Boolean} True if the node is a lifecycle method
|
||||
*/
|
||||
function isNodeALifeCycleMethod(node, checkAsyncSafeLifeCycles) {
|
||||
const nodeKeyName = (node.key || /** @type {ASTNode} */ ({})).name;
|
||||
|
||||
if (node.kind === 'constructor') {
|
||||
return true;
|
||||
}
|
||||
if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given node is inside a React Component lifecycle
|
||||
* method.
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {boolean} checkAsyncSafeLifeCycles
|
||||
* @return {Boolean} True if the node is inside a lifecycle method
|
||||
*/
|
||||
function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) {
|
||||
if ((node.type === 'MethodDefinition' || node.type === 'Property') && isNodeALifeCycleMethod(node, checkAsyncSafeLifeCycles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.parent) {
|
||||
return isInLifeCycleMethod(node.parent, checkAsyncSafeLifeCycles);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a function node is a setState updater
|
||||
* @param {ASTNode} node a function node
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isSetStateUpdater(node) {
|
||||
return node.parent.type === 'CallExpression' &&
|
||||
node.parent.callee.property &&
|
||||
node.parent.callee.property.name === 'setState' &&
|
||||
// Make sure we are in the updater not the callback
|
||||
node.parent.arguments[0] === node;
|
||||
}
|
||||
|
||||
function isPropArgumentInSetStateUpdater(context, name) {
|
||||
if (typeof name !== 'string') {
|
||||
return;
|
||||
}
|
||||
let scope = context.getScope();
|
||||
while (scope) {
|
||||
if (
|
||||
scope.block && scope.block.parent &&
|
||||
scope.block.parent.type === 'CallExpression' &&
|
||||
scope.block.parent.callee.property &&
|
||||
scope.block.parent.callee.property.name === 'setState' &&
|
||||
// Make sure we are in the updater not the callback
|
||||
scope.block.parent.arguments[0].start === scope.block.start &&
|
||||
scope.block.parent.arguments[0].params &&
|
||||
scope.block.parent.arguments[0].params.length > 1
|
||||
) {
|
||||
return scope.block.parent.arguments[0].params[1].name === name;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isInClassComponent(utils) {
|
||||
return utils.getParentES6Component() || utils.getParentES5Component();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is `this.props`
|
||||
* @param {ASTNode|undefined} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isThisDotProps(node) {
|
||||
return !!node &&
|
||||
node.type === 'MemberExpression' &&
|
||||
node.object.type === 'ThisExpression' &&
|
||||
node.property.name === 'props';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prop has spread operator.
|
||||
* @param {object} context
|
||||
* @param {ASTNode} node The AST node being marked.
|
||||
* @returns {Boolean} True if the prop has spread operator, false if not.
|
||||
*/
|
||||
function hasSpreadOperator(context, node) {
|
||||
const tokens = context.getSourceCode().getTokens(node);
|
||||
return tokens.length && tokens[0].value === '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the name of a property node
|
||||
* @param {ASTNode} node The AST node with the property.
|
||||
* @return {string|undefined} the name of the property or undefined if not found
|
||||
*/
|
||||
function getPropertyName(node) {
|
||||
const property = node.property;
|
||||
if (property) {
|
||||
switch (property.type) {
|
||||
case 'Identifier':
|
||||
if (node.computed) {
|
||||
return '__COMPUTED_PROP__';
|
||||
}
|
||||
return property.name;
|
||||
case 'MemberExpression':
|
||||
return;
|
||||
case 'Literal':
|
||||
// Accept computed properties that are literal strings
|
||||
if (typeof property.value === 'string') {
|
||||
return property.value;
|
||||
}
|
||||
// falls through
|
||||
default:
|
||||
if (node.computed) {
|
||||
return '__COMPUTED_PROP__';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a propTypes usage of the form `this.props.*`, `props.*`, `prevProps.*`, or `nextProps.*`.
|
||||
* @param {ASTNode} node
|
||||
* @param {Context} context
|
||||
* @param {Object} utils
|
||||
* @param {boolean} checkAsyncSafeLifeCycles
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) {
|
||||
if (isInClassComponent(utils)) {
|
||||
// this.props.*
|
||||
if (isThisDotProps(node.object)) {
|
||||
return true;
|
||||
}
|
||||
// props.* or prevProps.* or nextProps.*
|
||||
if (
|
||||
isCommonVariableNameForProps(node.object.name) &&
|
||||
(inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// this.setState((_, props) => props.*))
|
||||
if (isPropArgumentInSetStateUpdater(context, node.object.name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// props.* in function component
|
||||
return node.object.name === 'props' && !ast.isAssignmentLHS(node);
|
||||
}
|
||||
|
||||
module.exports = function usedPropTypesInstructions(context, components, utils) {
|
||||
const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0');
|
||||
|
||||
const propVariables = createPropVariables();
|
||||
const pushScope = propVariables.pushScope;
|
||||
const popScope = propVariables.popScope;
|
||||
|
||||
/**
|
||||
* Mark a prop type as used
|
||||
* @param {ASTNode} node The AST node being marked.
|
||||
* @param {string[]} [parentNames]
|
||||
*/
|
||||
function markPropTypesAsUsed(node, parentNames) {
|
||||
parentNames = parentNames || [];
|
||||
let type;
|
||||
let name;
|
||||
let allNames;
|
||||
let properties;
|
||||
switch (node.type) {
|
||||
case 'MemberExpression':
|
||||
name = getPropertyName(node);
|
||||
if (name) {
|
||||
allNames = parentNames.concat(name);
|
||||
if (
|
||||
// Match props.foo.bar, don't match bar[props.foo]
|
||||
node.parent.type === 'MemberExpression' &&
|
||||
node.parent.object === node
|
||||
) {
|
||||
markPropTypesAsUsed(node.parent, allNames);
|
||||
}
|
||||
// Handle the destructuring part of `const {foo} = props.a.b`
|
||||
if (
|
||||
node.parent.type === 'VariableDeclarator' &&
|
||||
node.parent.id.type === 'ObjectPattern'
|
||||
) {
|
||||
node.parent.id.parent = node.parent; // patch for bug in eslint@4 in which ObjectPattern has no parent
|
||||
markPropTypesAsUsed(node.parent.id, allNames);
|
||||
}
|
||||
|
||||
// const a = props.a
|
||||
if (
|
||||
node.parent.type === 'VariableDeclarator' &&
|
||||
node.parent.id.type === 'Identifier'
|
||||
) {
|
||||
propVariables.set(node.parent.id.name, allNames);
|
||||
}
|
||||
// Do not mark computed props as used.
|
||||
type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
|
||||
}
|
||||
break;
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression': {
|
||||
if (node.params.length === 0) {
|
||||
break;
|
||||
}
|
||||
type = 'destructuring';
|
||||
const propParam = isSetStateUpdater(node) ? node.params[1] : node.params[0];
|
||||
properties = propParam.type === 'AssignmentPattern' ?
|
||||
propParam.left.properties :
|
||||
propParam.properties;
|
||||
break;
|
||||
}
|
||||
case 'ObjectPattern':
|
||||
type = 'destructuring';
|
||||
properties = node.properties;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`);
|
||||
}
|
||||
|
||||
const component = components.get(utils.getParentComponent());
|
||||
const usedPropTypes = component && component.usedPropTypes || [];
|
||||
let ignoreUnusedPropTypesValidation = component && component.ignoreUnusedPropTypesValidation || false;
|
||||
|
||||
switch (type) {
|
||||
case 'direct': {
|
||||
// Ignore Object methods
|
||||
if (name in Object.prototype) {
|
||||
break;
|
||||
}
|
||||
|
||||
const reportedNode = node.property;
|
||||
usedPropTypes.push({
|
||||
name,
|
||||
allNames,
|
||||
node: reportedNode
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'destructuring': {
|
||||
for (let k = 0, l = (properties || []).length; k < l; k++) {
|
||||
if (hasSpreadOperator(context, properties[k]) || properties[k].computed) {
|
||||
ignoreUnusedPropTypesValidation = true;
|
||||
break;
|
||||
}
|
||||
const propName = ast.getKeyValue(context, properties[k]);
|
||||
|
||||
if (!propName || properties[k].type !== 'Property') {
|
||||
break;
|
||||
}
|
||||
|
||||
usedPropTypes.push({
|
||||
allNames: parentNames.concat([propName]),
|
||||
name: propName,
|
||||
node: properties[k]
|
||||
});
|
||||
|
||||
if (properties[k].value.type === 'ObjectPattern') {
|
||||
markPropTypesAsUsed(properties[k].value, parentNames.concat([propName]));
|
||||
} else if (properties[k].value.type === 'Identifier') {
|
||||
propVariables.set(propName, parentNames.concat(propName));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
components.set(component ? component.node : node, {
|
||||
usedPropTypes,
|
||||
ignoreUnusedPropTypesValidation
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function markDestructuredFunctionArgumentsAsUsed(node) {
|
||||
const param = node.params && isSetStateUpdater(node) ? node.params[1] : node.params[0];
|
||||
|
||||
const destructuring = param && (
|
||||
param.type === 'ObjectPattern' ||
|
||||
param.type === 'AssignmentPattern' && param.left.type === 'ObjectPattern'
|
||||
);
|
||||
|
||||
if (destructuring && (components.get(node) || components.get(node.parent))) {
|
||||
markPropTypesAsUsed(node);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetStateUpdater(node) {
|
||||
if (!node.params || node.params.length < 2 || !isSetStateUpdater(node)) {
|
||||
return;
|
||||
}
|
||||
markPropTypesAsUsed(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle both stateless functions and setState updater functions.
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function handleFunctionLikeExpressions(node) {
|
||||
pushScope();
|
||||
handleSetStateUpdater(node);
|
||||
markDestructuredFunctionArgumentsAsUsed(node);
|
||||
}
|
||||
|
||||
function handleCustomValidators(component) {
|
||||
const propTypes = component.declaredPropTypes;
|
||||
if (!propTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(propTypes).forEach((key) => {
|
||||
const node = propTypes[key].node;
|
||||
|
||||
if (node.value && astUtil.isFunctionLikeExpression(node.value)) {
|
||||
markPropTypesAsUsed(node.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
VariableDeclarator(node) {
|
||||
// let props = this.props
|
||||
if (isThisDotProps(node.init) && isInClassComponent(utils) && node.id.type === 'Identifier') {
|
||||
propVariables.set(node.id.name, []);
|
||||
}
|
||||
|
||||
// Only handles destructuring
|
||||
if (node.id.type !== 'ObjectPattern' || !node.init) {
|
||||
return;
|
||||
}
|
||||
|
||||
// let {props: {firstname}} = this
|
||||
const propsProperty = node.id.properties.find(property => (
|
||||
property.key &&
|
||||
(property.key.name === 'props' || property.key.value === 'props')
|
||||
));
|
||||
if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') {
|
||||
markPropTypesAsUsed(propsProperty.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// let {props} = this
|
||||
if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') {
|
||||
propVariables.set('props', []);
|
||||
return;
|
||||
}
|
||||
|
||||
// let {firstname} = props
|
||||
if (
|
||||
isCommonVariableNameForProps(node.init.name) &&
|
||||
(utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles))
|
||||
) {
|
||||
markPropTypesAsUsed(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// let {firstname} = this.props
|
||||
if (isThisDotProps(node.init) && isInClassComponent(utils)) {
|
||||
markPropTypesAsUsed(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// let {firstname} = thing, where thing is defined by const thing = this.props.**.*
|
||||
if (propVariables.get(node.init.name)) {
|
||||
markPropTypesAsUsed(node.id, propVariables.get(node.init.name));
|
||||
}
|
||||
},
|
||||
|
||||
FunctionDeclaration: handleFunctionLikeExpressions,
|
||||
|
||||
ArrowFunctionExpression: handleFunctionLikeExpressions,
|
||||
|
||||
FunctionExpression: handleFunctionLikeExpressions,
|
||||
|
||||
'FunctionDeclaration:exit': popScope,
|
||||
|
||||
'ArrowFunctionExpression:exit': popScope,
|
||||
|
||||
'FunctionExpression:exit': popScope,
|
||||
|
||||
JSXSpreadAttribute(node) {
|
||||
const component = components.get(utils.getParentComponent());
|
||||
components.set(component ? component.node : node, {
|
||||
ignoreUnusedPropTypesValidation: true
|
||||
});
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles)) {
|
||||
markPropTypesAsUsed(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (propVariables.get(node.object.name)) {
|
||||
markPropTypesAsUsed(node, propVariables.get(node.object.name));
|
||||
}
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
// If the object pattern is a destructured props object in a lifecycle
|
||||
// method -- mark it for used props.
|
||||
if (isNodeALifeCycleMethod(node.parent.parent, checkAsyncSafeLifeCycles) && node.properties.length > 0) {
|
||||
markPropTypesAsUsed(node.parent);
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit': function () {
|
||||
const list = components.list();
|
||||
|
||||
Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => {
|
||||
handleCustomValidators(list[component]);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for React components detection
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Search a particular variable in a list
|
||||
* @param {Array} variables The variables list.
|
||||
* @param {string} name The name of the variable to search.
|
||||
* @returns {Boolean} True if the variable was found, false if not.
|
||||
*/
|
||||
function findVariable(variables, name) {
|
||||
return variables.some(variable => variable.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return a particular variable in a list
|
||||
* @param {Array} variables The variables list.
|
||||
* @param {string} name The name of the variable to search.
|
||||
* @returns {Object} Variable if the variable was found, null if not.
|
||||
*/
|
||||
function getVariable(variables, name) {
|
||||
return variables.find(variable => variable.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all variable in a given scope
|
||||
*
|
||||
* Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21
|
||||
*
|
||||
* @param {Object} context The current rule context.
|
||||
* @returns {Array} The variables list
|
||||
*/
|
||||
function variablesInScope(context) {
|
||||
let scope = context.getScope();
|
||||
let variables = scope.variables;
|
||||
|
||||
while (scope.type !== 'global') {
|
||||
scope = scope.upper;
|
||||
variables = scope.variables.concat(variables);
|
||||
}
|
||||
if (scope.childScopes.length) {
|
||||
variables = scope.childScopes[0].variables.concat(variables);
|
||||
if (scope.childScopes[0].childScopes.length) {
|
||||
variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
|
||||
}
|
||||
}
|
||||
variables.reverse();
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable by name in the current scope.
|
||||
* @param {Object} context The current rule context.
|
||||
* @param {string} name Name of the variable to look for.
|
||||
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
|
||||
*/
|
||||
function findVariableByName(context, name) {
|
||||
const variable = getVariable(variablesInScope(context), name);
|
||||
|
||||
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (variable.defs[0].node.type === 'TypeAlias') {
|
||||
return variable.defs[0].node.right;
|
||||
}
|
||||
|
||||
return variable.defs[0].node.init;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findVariable,
|
||||
findVariableByName,
|
||||
getVariable,
|
||||
variablesInScope
|
||||
};
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for React and Flow version configuration
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const resolve = require('resolve');
|
||||
const error = require('./error');
|
||||
|
||||
let warnedForMissingVersion = false;
|
||||
|
||||
function resetWarningFlag() {
|
||||
warnedForMissingVersion = false;
|
||||
}
|
||||
|
||||
function detectReactVersion() {
|
||||
try {
|
||||
const reactPath = resolve.sync('react', {basedir: process.cwd()});
|
||||
const react = require(reactPath); // eslint-disable-line global-require, import/no-dynamic-require
|
||||
return react.version;
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
if (!warnedForMissingVersion) {
|
||||
error('Warning: React version was set to "detect" in eslint-plugin-react settings, ' +
|
||||
'but the "react" package is not installed. Assuming latest React version for linting.');
|
||||
warnedForMissingVersion = true;
|
||||
}
|
||||
return '999.999.999';
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function getReactVersionFromContext(context) {
|
||||
let confVer = '999.999.999';
|
||||
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
|
||||
if (context.settings.react && context.settings.react.version) {
|
||||
let settingsVersion = context.settings.react.version;
|
||||
if (settingsVersion === 'detect') {
|
||||
settingsVersion = detectReactVersion();
|
||||
}
|
||||
if (typeof settingsVersion !== 'string') {
|
||||
error('Warning: React version specified in eslint-plugin-react-settings must be a string; ' +
|
||||
`got “${typeof settingsVersion}”`);
|
||||
}
|
||||
confVer = String(settingsVersion);
|
||||
} else if (!warnedForMissingVersion) {
|
||||
error('Warning: React version not specified in eslint-plugin-react settings. ' +
|
||||
'See https://github.com/yannickcr/eslint-plugin-react#configuration .');
|
||||
warnedForMissingVersion = true;
|
||||
}
|
||||
confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
|
||||
return confVer.split('.').map(part => Number(part));
|
||||
}
|
||||
|
||||
function detectFlowVersion() {
|
||||
try {
|
||||
const flowPackageJsonPath = resolve.sync('flow-bin/package.json', {basedir: process.cwd()});
|
||||
const flowPackageJson = require(flowPackageJsonPath); // eslint-disable-line global-require, import/no-dynamic-require
|
||||
return flowPackageJson.version;
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
error('Warning: Flow version was set to "detect" in eslint-plugin-react settings, ' +
|
||||
'but the "flow-bin" package is not installed. Assuming latest Flow version for linting.');
|
||||
return '999.999.999';
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function getFlowVersionFromContext(context) {
|
||||
let confVer = '999.999.999';
|
||||
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
|
||||
if (context.settings.react && context.settings.react.flowVersion) {
|
||||
let flowVersion = context.settings.react.flowVersion;
|
||||
if (flowVersion === 'detect') {
|
||||
flowVersion = detectFlowVersion();
|
||||
}
|
||||
if (typeof flowVersion !== 'string') {
|
||||
error('Warning: Flow version specified in eslint-plugin-react-settings must be a string; ' +
|
||||
`got “${typeof flowVersion}”`);
|
||||
}
|
||||
confVer = String(flowVersion);
|
||||
} else {
|
||||
throw 'Could not retrieve flowVersion from settings'; // eslint-disable-line no-throw-literal
|
||||
}
|
||||
confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer;
|
||||
return confVer.split('.').map(part => Number(part));
|
||||
}
|
||||
|
||||
function normalizeParts(parts) {
|
||||
return Array.from({length: 3}, (_, i) => (parts[i] || 0));
|
||||
}
|
||||
|
||||
function test(context, methodVer, confVer) {
|
||||
const methodVers = normalizeParts(String(methodVer || '').split('.').map(part => Number(part)));
|
||||
const confVers = normalizeParts(confVer);
|
||||
const higherMajor = methodVers[0] < confVers[0];
|
||||
const higherMinor = methodVers[0] === confVers[0] && methodVers[1] < confVers[1];
|
||||
const higherOrEqualPatch = methodVers[0] === confVers[0] &&
|
||||
methodVers[1] === confVers[1] &&
|
||||
methodVers[2] <= confVers[2];
|
||||
|
||||
return higherMajor || higherMinor || higherOrEqualPatch;
|
||||
}
|
||||
|
||||
function testReactVersion(context, methodVer) {
|
||||
return test(context, methodVer, getReactVersionFromContext(context));
|
||||
}
|
||||
|
||||
function testFlowVersion(context, methodVer) {
|
||||
return test(context, methodVer, getFlowVersionFromContext(context));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testReactVersion,
|
||||
testFlowVersion,
|
||||
resetWarningFlag
|
||||
};
|
||||
Reference in New Issue
Block a user