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.
182 lines
4.5 KiB
182 lines
4.5 KiB
'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, |
|
};
|
|
|