applying transfer to react app

This commit is contained in:
Tyler Koenig
2021-09-20 16:54:47 -04:00
parent 8819f31dd0
commit c612b7d702
37373 changed files with 3775588 additions and 2871 deletions
+970
View File
@@ -0,0 +1,970 @@
/**
* @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');
const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
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.filter((newProp) => {
const newPropIsAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp));
return !newPropIsAlreadyInTheList;
});
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 getWrapperFunctions(context, pragma) {
const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
// eslint-disable-next-line arrow-body-style
return componentWrapperFunctions.map((wrapperFunction) => {
return typeof wrapperFunction === 'string'
? {property: wrapperFunction}
: Object.assign({}, wrapperFunction, {
object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object
});
}).concat([
{property: 'forwardRef', object: pragma},
{property: 'memo', object: pragma}
]);
}
function componentRule(rule, context) {
const createClass = pragmaUtil.getCreateClassFromContext(context);
const pragma = pragmaUtil.getFromContext(context);
const sourceCode = context.getSourceCode();
const components = new Components();
const wrapperFunctions = getWrapperFunctions(context, pragma);
// 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 latestDef = variableUtil.getLatestVariableDefinition(variableInScope);
if (latestDef) {
// check if latest definition is a variable declaration: 'variable = value'
if (latestDef.node.type === 'VariableDeclarator' && latestDef.node.init) {
// check for: 'variable = pragma.variable'
if (
latestDef.node.init.type === 'MemberExpression'
&& latestDef.node.init.object.type === 'Identifier'
&& latestDef.node.init.object.name === pragma
) {
return true;
}
// check for: '{variable} = pragma'
if (
latestDef.node.init.type === 'Identifier'
&& latestDef.node.init.name === pragma
) {
return true;
}
// "require('react')"
let requireExpression = null;
// get "require('react')" from: "{variable} = require('react')"
if (latestDef.node.init.type === 'CallExpression') {
requireExpression = latestDef.node.init;
}
// get "require('react')" from: "variable = require('react').variable"
if (
!requireExpression
&& latestDef.node.init.type === 'MemberExpression'
&& latestDef.node.init.object.type === 'CallExpression'
) {
requireExpression = latestDef.node.init.object;
}
// check proper require.
if (
requireExpression
&& requireExpression.callee
&& requireExpression.callee.name === 'require'
&& requireExpression.arguments[0]
&& requireExpression.arguments[0].value === pragma.toLocaleLowerCase()
) {
return true;
}
return false;
}
// latest definition is an import declaration: import {<variable>} from 'react'
if (
latestDef.parent
&& latestDef.parent.type === 'ImportDeclaration'
&& latestDef.parent.source.value === pragma.toLocaleLowerCase()
) {
return true;
}
}
}
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) {
// match `React.createElement()`
if (
node
&& node.callee
&& node.callee.object
&& node.callee.object.name === pragma
&& node.callee.property
&& node.callee.property.name === 'createElement'
) {
return true;
}
// match `createElement()`
if (
node
&& node.callee
&& node.callee.name === 'createElement'
&& this.isDestructuredFromPragmaImport('createElement')
) {
return true;
}
return false;
},
/**
* 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';
},
isReturningJSX(ASTNode, strict) {
return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true);
},
isReturningJSXOrNull(ASTNode, strict) {
return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict);
},
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
&& jsxElement.argument
&& 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 && 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;
}
return wrapperFunctions.some((wrapperFunction) => {
if (node.callee.type === 'MemberExpression') {
return wrapperFunction.object
&& wrapperFunction.object === node.callee.object.name
&& wrapperFunction.property === node.callee.property.name
&& !this.nodeWrapsComponent(node);
}
return wrapperFunction.property === node.callee.name
&& (!wrapperFunction.object
// Functions coming from the current pragma need special handling
|| (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name))
);
});
},
/**
* Find a return statement in the current node
*
* @param {ASTNode} node 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':
case 'ArrowFunctionExpression': {
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) {
const parent = node.parent;
if (
node.type === 'FunctionDeclaration'
&& (!node.id || isFirstLetterCapitalized(node.id.name))
&& utils.isReturningJSXOrNull(node)
) {
return node;
}
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
const isMethod = parent.type === 'Property' && parent.method;
const isPropertyAssignment = parent.type === 'AssignmentExpression'
&& parent.left.type === 'MemberExpression';
const isModuleExportsAssignment = isPropertyAssignment
&& parent.left.object.name === 'module'
&& parent.left.property.name === 'exports';
if (node.parent.type === 'ExportDefaultDeclaration') {
if (utils.isReturningJSX(node)) {
return node;
}
return undefined;
}
if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
if (isFirstLetterCapitalized(node.parent.id.name)) {
return node;
}
return undefined;
}
// Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
if (pragmaComponentWrapper) {
return pragmaComponentWrapper;
}
if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) {
return undefined;
}
if (utils.isParentComponentNotStatelessComponent(node)) {
return undefined;
}
if (isMethod && !isFirstLetterCapitalized(node.parent.key.name)) {
return utils.isReturningJSX(node) ? node : undefined;
}
if (node.id) {
return isFirstLetterCapitalized(node.id.name) ? node : undefined;
}
if (
isPropertyAssignment
&& !isModuleExportsAssignment
&& !isFirstLetterCapitalized(parent.left.property.name)
) {
return undefined;
}
return node;
}
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);
},
isParentComponentNotStatelessComponent(node) {
return !!(
node.parent
&& node.parent.key
&& node.parent.key.type === 'Identifier'
// custom component functions must start with a capital letter (returns false otherwise)
&& node.parent.key.name.charAt(0) === node.parent.key.name.charAt(0).toLowerCase()
// react render function cannot have params
&& !!(node.params || []).length
);
}
};
// 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] = (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
View File
@@ -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
};
+383
View File
@@ -0,0 +1,383 @@
/**
* @fileoverview Utility functions for AST
*/
'use strict';
const estraverse = require('estraverse');
// const pragmaUtil = require('./pragma');
/**
* Wrapper for estraverse.traverse
*
* @param {ASTNode} ASTnode The AST node being checked
* @param {Object} visitor Visitor Object for estraverse
*/
function traverse(ASTnode, visitor) {
const opts = Object.assign({}, {
fallback(node) {
return Object.keys(node).filter((key) => key === 'children' || key === 'argument');
}
}, visitor);
opts.keys = Object.assign({}, visitor.keys, {
JSXElement: ['children'],
JSXFragment: ['children']
});
estraverse.traverse(ASTnode, opts);
}
/**
* 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));
}
/**
* Helper function for traversing "returns" (return statements or the
* returned expression in the case of an arrow function) of a function
*
* @param {ASTNode} ASTNode The AST node being checked
* @param {Context} context The context of `ASTNode`.
* @param {function} enterFunc Function to execute for each returnStatement found
* @returns {undefined}
*/
function traverseReturns(ASTNode, context, enterFunc) {
const nodeType = ASTNode.type;
if (nodeType === 'ReturnStatement') {
return enterFunc(ASTNode);
}
if (nodeType === 'ArrowFunctionExpression' && ASTNode.expression) {
return enterFunc(ASTNode.body);
}
/* TODO: properly warn on React.forwardRefs having typo properties
if (nodeType === 'CallExpression') {
const callee = ASTNode.callee;
const pragma = pragmaUtil.getFromContext(context);
if (
callee.type === 'MemberExpression'
&& callee.object.type === 'Identifier'
&& callee.object.name === pragma
&& callee.property.type === 'Identifier'
&& callee.property.name === 'forwardRef'
&& ASTNode.arguments.length > 0
) {
return enterFunc(ASTNode.arguments[0]);
}
return;
}
*/
if (
nodeType !== 'FunctionExpression'
&& nodeType !== 'FunctionDeclaration'
&& nodeType !== 'ArrowFunctionExpression'
&& nodeType !== 'MethodDefinition'
) {
return;
}
traverse(ASTNode.body, {
enter(node) {
switch (node.type) {
case 'ReturnStatement':
this.skip();
return enterFunc(node);
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
return this.skip();
default:
}
}
});
}
/**
* 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 | undefined} 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)
);
}
if (node.type === 'GenericTypeAnnotation') {
return node.id.name;
}
if (node.type === 'ObjectTypeAnnotation') {
return;
}
const key = node.key || node.argument;
if (!key) {
return;
}
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
);
}
/**
* Extracts the expression node that is wrapped inside a TS type assertion
*
* @param {ASTNode} node - potential TS node
* @returns {ASTNode} - unwrapped expression node
*/
function unwrapTSAsExpression(node) {
if (node && node.type === 'TSAsExpression') return node.expression;
return node;
}
function isTSTypeReference(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeReference';
}
function isTSTypeAnnotation(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAnnotation';
}
function isTSTypeLiteral(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeLiteral';
}
function isTSIntersectionType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSIntersectionType';
}
function isTSInterfaceHeritage(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSInterfaceHeritage';
}
function isTSInterfaceDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSInterfaceDeclaration';
}
function isTSTypeAliasDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAliasDeclaration';
}
function isTSParenthesizedType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeAliasDeclaration';
}
function isTSFunctionType(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSFunctionType';
}
function isTSTypeQuery(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeQuery';
}
function isTSTypeParameterInstantiation(node) {
if (!node) return false;
const nodeType = node.type;
return nodeType === 'TSTypeParameterInstantiation';
}
module.exports = {
traverse,
findReturnStatement,
getFirstNodeInLine,
getPropertyName,
getPropertyNameNode,
getComponentProperties,
getKeyValue,
isAssignmentLHS,
isClass,
isFunction,
isFunctionLikeExpression,
isNodeFirstInLine,
unwrapTSAsExpression,
traverseReturns,
isTSTypeReference,
isTSTypeAnnotation,
isTSTypeLiteral,
isTSIntersectionType,
isTSInterfaceHeritage,
isTSInterfaceDeclaration,
isTSTypeAliasDeclaration,
isTSParenthesizedType,
isTSFunctionType,
isTSTypeQuery,
isTSTypeParameterInstantiation
};
+267
View File
@@ -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('./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
View File
@@ -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
View File
@@ -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;
@@ -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;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
/**
* Check if the first letter of a string is capitalized.
* @param {String} word String to check
* @returns {Boolean} True if first letter is capitalized.
*/
function isFirstLetterCapitalized(word) {
if (!word) {
return false;
}
const firstLetter = word.charAt(0);
return firstLetter.toUpperCase() === firstLetter;
}
module.exports = isFirstLetterCapitalized;
+157
View File
@@ -0,0 +1,157 @@
/**
* @fileoverview Utility functions for JSX
*/
'use strict';
const estraverse = require('estraverse');
const elementType = require('jsx-ast-utils/elementType');
const astUtil = require('./ast');
// See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js
// for why we only test for the first character
const COMPAT_TAG_REGEX = /^[a-z]/;
/**
* Checks if a node represents a DOM element according to React.
* @param {object} node - JSXOpeningElement to check.
* @returns {boolean} Whether or not the node corresponds to a DOM element.
*/
function isDOMComponent(node) {
const name = elementType(node);
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;
}
/**
* Check if the node is returning JSX or null
*
* @param {Function} isCreateElement Function to determine if a CallExpresion is
* a createElement one
* @param {ASTNode} ASTnode The AST node being checked
* @param {Context} context The context of `ASTNode`.
* @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
* @param {Boolean} [ignoreNull] If true, null return values will be ignored
* @returns {Boolean} True if the node is returning JSX or null, false if not
*/
function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) {
let found = false;
astUtil.traverseReturns(ASTnode, context, (node) => {
// Traverse return statement
astUtil.traverse(node, {
enter(childNode) {
const setFound = () => {
found = true;
this.skip();
};
switch (childNode.type) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
// Do not traverse into inner function definitions
return this.skip();
case 'ConditionalExpression':
if (!strict) break;
if (isJSX(childNode.consequent) && isJSX(childNode.alternate)) {
setFound();
}
this.skip();
break;
case 'LogicalExpression':
if (!strict) break;
if (isJSX(childNode.left) && isJSX(childNode.right)) {
setFound();
}
this.skip();
break;
case 'JSXElement':
case 'JSXFragment':
setFound(); break;
case 'CallExpression':
if (isCreateElement(childNode)) {
setFound();
}
break;
case 'Literal':
if (!ignoreNull && childNode.value === null) {
setFound();
}
break;
default:
}
}
});
return found && estraverse.VisitorOption.Break;
});
return found;
}
module.exports = {
isDOMComponent,
isFragment,
isJSX,
isJSXAttributeKey,
isWhiteSpaces,
isReturningJSX
};
+46
View File
@@ -0,0 +1,46 @@
/**
* @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';
/** TODO: type {(string | { name: string, formAttribute: string })[]} */
/** @type {any} */
const DEFAULT_FORM_COMPONENTS = ['form'];
const DEFAULT_FORM_ATTRIBUTE = 'action';
function getFormComponents(context) {
const settings = context.settings || {};
const formComponents = /** @type {typeof DEFAULT_FORM_COMPONENTS} */ (
DEFAULT_FORM_COMPONENTS.concat(settings.formComponents || [])
);
return new Map(formComponents.map((value) => {
if (typeof value === 'string') {
return [value, DEFAULT_FORM_ATTRIBUTE];
}
return [value.name, value.formAttribute];
}));
}
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 = {
getFormComponents,
getLinkComponents
};
+14
View File
@@ -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;
+104
View File
@@ -0,0 +1,104 @@
/**
* @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))
},
messages: {
noSetState: 'Do not use setState in {{name}}'
},
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,
messageId: 'noSetState',
data: {
name: ancestor.key.name
}
});
return true;
});
}
};
}
};
}
module.exports = makeNoMethodSetStateRule;
+60
View File
@@ -0,0 +1,60 @@
/**
* @fileoverview Utility functions for React pragma configuration
* @author Yannick Croissant
*/
'use strict';
const JSX_ANNOTATION_REGEX = /@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
};
File diff suppressed because it is too large Load Diff
+164
View File
@@ -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
};
+58
View File
@@ -0,0 +1,58 @@
/**
* @fileoverview Utility functions for propWrapperFunctions setting
*/
'use strict';
function searchPropWrapperFunctions(name, propWrapperFunctions) {
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;
});
}
function getPropWrapperFunctions(context) {
return new Set(context.settings.propWrapperFunctions || []);
}
function isPropWrapperFunction(context, name) {
if (typeof name !== 'string') {
return false;
}
const propWrapperFunctions = getPropWrapperFunctions(context);
return searchPropWrapperFunctions(name, propWrapperFunctions);
}
function getExactPropWrapperFunctions(context) {
const propWrapperFunctions = getPropWrapperFunctions(context);
const exactPropWrappers = Array.from(propWrapperFunctions).filter((func) => func.exact === true);
return new Set(exactPropWrappers);
}
function isExactPropWrapperFunction(context, name) {
const exactPropWrappers = getExactPropWrapperFunctions(context);
return searchPropWrapperFunctions(name, exactPropWrappers);
}
function formatPropWrapperFunctions(propWrapperFunctions) {
return Array.from(propWrapperFunctions, (func) => {
if (func.object && func.property) {
return `'${func.object}.${func.property}'`;
}
if (func.property) {
return `'${func.property}'`;
}
return `'${func}'`;
}).join(', ');
}
module.exports = {
formatPropWrapperFunctions,
getExactPropWrapperFunctions,
getPropWrapperFunctions,
isExactPropWrapperFunction,
isPropWrapperFunction
};
+102
View File
@@ -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
};
+562
View File
@@ -0,0 +1,562 @@
/**
* @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) {
const unwrappedParentCalleeNode = node.parent.type === 'CallExpression'
&& ast.unwrapTSAsExpression(node.parent.callee);
return unwrappedParentCalleeNode
&& unwrappedParentCalleeNode.property
&& unwrappedParentCalleeNode.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) {
const unwrappedParentCalleeNode = scope.block
&& scope.block.parent
&& scope.block.parent.type === 'CallExpression'
&& ast.unwrapTSAsExpression(scope.block.parent.callee);
if (
unwrappedParentCalleeNode
&& unwrappedParentCalleeNode.property
&& unwrappedParentCalleeNode.property.name === 'setState'
// Make sure we are in the updater not the callback
&& scope.block.parent.arguments[0].range[0] === scope.block.range[0]
&& 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'
&& ast.unwrapTSAsExpression(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 === '...';
}
/**
* 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) {
const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object);
if (isInClassComponent(utils)) {
// this.props.*
if (isThisDotProps(unwrappedObjectNode)) {
return true;
}
// props.* or prevProps.* or nextProps.*
if (
isCommonVariableNameForProps(unwrappedObjectNode.name)
&& (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor())
) {
return true;
}
// this.setState((_, props) => props.*))
if (isPropArgumentInSetStateUpdater(context, unwrappedObjectNode.name)) {
return true;
}
return false;
}
// props.* in function component
return unwrappedObjectNode.name === 'props' && !ast.isAssignmentLHS(node);
}
/**
* Retrieve the name of a property node
* @param {ASTNode} node The AST node with the property.
* @param {Context} context
* @param {Object} utils
* @param {boolean} checkAsyncSafeLifeCycles
* @return {string|undefined} the name of the property or undefined if not found
*/
function getPropertyName(node, context, utils, checkAsyncSafeLifeCycles) {
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;
}
// Accept number as well but only accept props[123]
if (typeof property.value === 'number') {
if (isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles)) {
return property.raw;
}
}
// falls through
default:
if (node.computed) {
return '__COMPUTED_PROP__';
}
break;
}
}
}
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 'OptionalMemberExpression':
case 'MemberExpression':
name = getPropertyName(node, context, utils, checkAsyncSafeLifeCycles);
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;
case 'TSEmptyBodyFunctionExpression':
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) {
const unwrappedInitNode = ast.unwrapTSAsExpression(node.init);
// let props = this.props
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') {
propVariables.set(node.id.name, []);
}
// Only handles destructuring
if (node.id.type !== 'ObjectPattern' || !unwrappedInitNode) {
return;
}
// let {props: {firstname}} = this
const propsProperty = node.id.properties.find((property) => (
property.key
&& (property.key.name === 'props' || property.key.value === 'props')
));
if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') {
markPropTypesAsUsed(propsProperty.value);
return;
}
// let {props} = this
if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') {
propVariables.set('props', []);
return;
}
// let {firstname} = props
if (
isCommonVariableNameForProps(unwrappedInitNode.name)
&& (utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles))
) {
markPropTypesAsUsed(node.id);
return;
}
// let {firstname} = this.props
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) {
markPropTypesAsUsed(node.id);
return;
}
// let {firstname} = thing, where thing is defined by const thing = this.props.**.*
if (propVariables.get(unwrappedInitNode.name)) {
markPropTypesAsUsed(node.id, propVariables.get(unwrappedInitNode.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, OptionalMemberExpression'(node) {
if (isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles)) {
markPropTypesAsUsed(node);
return;
}
const propVariable = propVariables.get(ast.unwrapTSAsExpression(node.object).name);
if (propVariable) {
markPropTypesAsUsed(node, propVariable);
}
},
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'() {
const list = components.list();
Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => {
handleCustomValidators(list[component]);
});
}
};
};
+90
View File
@@ -0,0 +1,90 @@
/**
* @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;
}
/**
* Returns the latest definition of the variable.
* @param {Object} variable
* @returns {Object | undefined} The latest variable definition or undefined.
*/
function getLatestVariableDefinition(variable) {
return variable.defs[variable.defs.length - 1];
}
module.exports = {
findVariable,
findVariableByName,
getVariable,
variablesInScope,
getLatestVariableDefinition
};
+161
View File
@@ -0,0 +1,161 @@
/**
* @fileoverview Utility functions for React and Flow version configuration
* @author Yannick Croissant
*/
'use strict';
const fs = require('fs');
const resolve = require('resolve');
const path = require('path');
const error = require('./error');
let warnedForMissingVersion = false;
function resetWarningFlag() {
warnedForMissingVersion = false;
}
let cachedDetectedReactVersion;
function resetDetectedVersion() {
cachedDetectedReactVersion = undefined;
}
function resolveBasedir(contextOrFilename) {
if (contextOrFilename) {
const filename = typeof contextOrFilename === 'string' ? contextOrFilename : contextOrFilename.getFilename();
const dirname = path.dirname(filename);
try {
if (fs.statSync(filename).isFile()) {
// dirname must be dir here
return dirname;
}
} catch (err) {
// https://github.com/eslint/eslint/issues/11989
if (err.code === 'ENOTDIR') {
// virtual filename could be recursive
return resolveBasedir(dirname);
}
}
}
return process.cwd();
}
// TODO, semver-major: remove context fallback
function detectReactVersion(context) {
if (cachedDetectedReactVersion) {
return cachedDetectedReactVersion;
}
const basedir = resolveBasedir(context);
try {
const reactPath = resolve.sync('react', {basedir});
const react = require(reactPath); // eslint-disable-line global-require, import/no-dynamic-require
cachedDetectedReactVersion = react.version;
return cachedDetectedReactVersion;
} 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;
}
cachedDetectedReactVersion = '999.999.999';
return cachedDetectedReactVersion;
}
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 && context.settings.react && context.settings.react.version) {
let settingsVersion = context.settings.react.version;
if (settingsVersion === 'detect') {
settingsVersion = detectReactVersion(context);
}
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));
}
// TODO, semver-major: remove context fallback
function detectFlowVersion(context) {
const basedir = resolveBasedir(context);
try {
const flowPackageJsonPath = resolve.sync('flow-bin/package.json', {basedir});
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(context);
}
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,
resetDetectedVersion
};