You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
4.5 KiB

2 years ago
'use strict';
const doctrine = require('doctrine');
const pragmaUtil = require('./pragma');
// eslint-disable-next-line valid-jsdoc
/**
* @template {(_: object) => any} T
* @param {T} fn
* @returns {T}
*/
function memoize(fn) {
const cache = new WeakMap();
// @ts-ignore
return function memoizedFn(arg) {
const cachedValue = cache.get(arg);
if (cachedValue !== undefined) {
return cachedValue;
}
const v = fn(arg);
cache.set(arg, v);
return v;
};
}
const getPragma = memoize(pragmaUtil.getFromContext);
const getCreateClass = memoize(pragmaUtil.getCreateClassFromContext);
/**
* @param {ASTNode} node
* @param {Context} context
* @returns {boolean}
*/
function isES5Component(node, context) {
const pragma = getPragma(context);
const createClass = getCreateClass(context);
if (!node.parent || !node.parent.callee) {
return false;
}
const callee = node.parent.callee;
// React.createClass({})
if (callee.type === 'MemberExpression') {
return callee.object.name === pragma && callee.property.name === createClass;
}
// createClass({})
if (callee.type === 'Identifier') {
return callee.name === createClass;
}
return false;
}
/**
* Check if the node is explicitly declared as a descendant of a React Component
* @param {any} node
* @param {Context} context
* @returns {boolean}
*/
function isExplicitComponent(node, context) {
const sourceCode = context.getSourceCode();
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;
}
let commentAst;
try {
commentAst = doctrine.parse(comment.value, {
unwrap: true,
tags: ['extends', 'augments'],
});
} catch (e) {
// handle a bug in the archived `doctrine`, see #2596
return false;
}
const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
return relevantTags.length > 0;
}
/**
* @param {ASTNode} node
* @param {Context} context
* @returns {boolean}
*/
function isES6Component(node, context) {
const pragma = getPragma(context);
if (isExplicitComponent(node, context)) {
return true;
}
if (!node.superClass) {
return false;
}
if (node.superClass.type === 'MemberExpression') {
return node.superClass.object.name === pragma
&& /^(Pure)?Component$/.test(node.superClass.property.name);
}
if (node.superClass.type === 'Identifier') {
return /^(Pure)?Component$/.test(node.superClass.name);
}
return false;
}
/**
* Get the parent ES5 component node from the current scope
* @param {Context} context
* @returns {ASTNode|null}
*/
function getParentES5Component(context) {
let scope = context.getScope();
while (scope) {
// @ts-ignore
const node = scope.block && scope.block.parent && scope.block.parent.parent;
if (node && isES5Component(node, context)) {
return node;
}
scope = scope.upper;
}
return null;
}
/**
* Get the parent ES6 component node from the current scope
* @param {Context} context
* @returns {ASTNode | null}
*/
function getParentES6Component(context) {
let scope = context.getScope();
while (scope && scope.type !== 'class') {
scope = scope.upper;
}
const node = scope && scope.block;
if (!node || !isES6Component(node, context)) {
return null;
}
return node;
}
/**
* Checks if a component extends React.PureComponent
* @param {ASTNode} node
* @param {Context} context
* @returns {boolean}
*/
function isPureComponent(node, context) {
const pragma = getPragma(context);
const sourceCode = context.getSourceCode();
if (node.superClass) {
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
}
return false;
}
/**
* @param {ASTNode} node
* @returns {boolean}
*/
function isStateMemberExpression(node) {
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
}
module.exports = {
isES5Component,
isES6Component,
getParentES5Component,
getParentES6Component,
isExplicitComponent,
isPureComponent,
isStateMemberExpression,
};