eslint-plugin-react-native-unistyles
Version:
React Native Unistyles rules for ESLint
326 lines (325 loc) • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.componentsUtils = exports.enhanceRuleWithComponentDetection = exports.Components = void 0;
const utils_1 = require("@typescript-eslint/utils");
class Components {
list = {};
/**
* Get unique identifier for a node
*/
getId(node) {
return node.range?.join(':');
}
/**
* Add a node to the components list, or update it if it's already in the list
* 0=banned, 1=maybe, 2=yes
*/
add(node, confidence) {
const id = this.getId(node);
if (!id)
return;
if (this.list[id]) {
if (confidence === 0 || this.list[id].confidence === 0) {
this.list[id].confidence = 0;
}
else {
this.list[id].confidence = Math.max(this.list[id].confidence, confidence);
}
return;
}
this.list[id] = { node, confidence };
}
/**
* Find a component in the list using its node
*/
get(node) {
const id = this.getId(node);
if (!id)
return;
return this.list[id];
}
/**
* Update a component in the list
*/
set(node, props) {
let currentNode = node;
while (currentNode) {
const id = this.getId(currentNode);
if (id && this.list[id]) {
break;
}
currentNode = node.parent;
}
if (!currentNode) {
return;
}
const id = this.getId(currentNode);
if (!id)
return;
const existingItem = this.list[id];
if (existingItem) {
this.list[id] = { ...existingItem, ...props };
}
}
/**
* Return the components list
* Components for which we are not confident are not returned
*/
all() {
const list = {};
for (const entry of Object.entries(this.list)) {
const [id, item] = entry;
if (item.confidence >= 2) {
list[id] = item;
}
}
return list;
}
/**
* Return the length of the components list
* Components for which we are not confident are not counted
*/
length() {
return Object.values(this.list).filter((item) => item.confidence >= 2).length;
}
}
exports.Components = Components;
const createUtils = (context, sourceCode) => {
// Utilities for component detection
const utils = {
/**
* Check if the node is a React ES5 component
*
* @param {TSESTree.Node} node The AST node being checked.
* returns True if the node is a React ES5 component, false if not
*/
isES5Component(node) {
if (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
return /^(React\.)?createClass$/.test(sourceCode.getText(node.parent.callee));
}
return false;
},
/**
* Check if the node is a React ES6 component
*
* @param {TSESTree.Node} node The AST node being checked.
* returns True if the node is a React ES6 component, false if not
*/
isES6Component(node) {
if (node.type === utils_1.AST_NODE_TYPES.ClassDeclaration || node.type === utils_1.AST_NODE_TYPES.ClassExpression) {
const superClass = node.superClass;
if (!superClass)
return false;
return /^(React\.)?(Pure)?Component$/.test(sourceCode.getText(superClass));
}
return false;
},
/**
* Check if the return statement is returning JSX
*
* @param {TSESTree.Node} node The AST node being checked (must be a ReturnStatement).
* returns True if the return statement is returning JSX, false if not
*/
isJSXReturnStatement(node) {
const returnsJSX = node.argument?.type === utils_1.AST_NODE_TYPES.JSXElement || node.argument?.type === utils_1.AST_NODE_TYPES.JSXFragment;
const returnsReactCreateElement = node.argument?.type === utils_1.AST_NODE_TYPES.CallExpression
&& node.argument.callee.type === utils_1.AST_NODE_TYPES.MemberExpression
&& node.argument.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
&& node.argument.callee.property.name === 'createElement';
return returnsJSX || returnsReactCreateElement;
},
/**
* Check if the function is returning JSX
*
* @param {TSESTree.Node} node The AST node being checked (must be a ReturnStatement).
* returns True if the function is returning JSX, false if not
*/
isArrowFunctionReturningJSXDirectly(node) {
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
const returnsJSX = node.body?.type === utils_1.AST_NODE_TYPES.JSXElement || node.body?.type === utils_1.AST_NODE_TYPES.JSXFragment;
const returnsReactCreateElement = node.body?.type === utils_1.AST_NODE_TYPES.CallExpression
&& node.body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression
&& node.body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
&& node.body.callee.property.name === 'createElement';
return returnsJSX || returnsReactCreateElement;
}
return false;
},
/**
* Get the parent component node from the current scope
*
* returns component node, null if we are not in a component
*/
getParentComponent(_n) {
return (utils.getParentStatelessComponent(_n)
|| utils.getParentES6Component(_n)
|| utils.getParentES5Component(_n));
},
/**
* Get the parent ES5 component node from the current scope
*
* returns component node, null if we are not in a component
*/
getParentES5Component(_n) {
let scope = (context.sourceCode || context).getScope(_n);
while (scope) {
const node = scope.block && scope.block.parent && scope.block.parent.parent;
if (node && utils.isES5Component(node)) {
return node;
}
if (!scope.upper) {
return null;
}
scope = scope.upper;
}
return null;
},
/**
* Get the parent ES6 component node from the current scope
*
* returns component node, null if we are not in a component
*/
getParentES6Component(_n) {
let scope = (context.sourceCode || context).getScope(_n);
while (scope && scope.type !== 'class') {
if (!scope.upper) {
break;
}
scope = scope.upper;
}
const node = scope.block;
if (!node || !utils.isES6Component(node)) {
return null;
}
return node;
},
/**
* Get the parent stateless component node from the current scope
*
* returns component node, null if we are not in a component
*/
getParentStatelessComponent(_n) {
let scope = (context.sourceCode || context).getScope(_n);
while (scope) {
const node = scope.block;
// Ignore non functions
const isFunction = node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration
|| node.type === utils_1.AST_NODE_TYPES.FunctionExpression
|| node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
// Ignore classes methods
const parentIsNotMethod = !node.parent || node.parent.type !== 'MethodDefinition';
if (isFunction && parentIsNotMethod) {
return node;
}
if (!scope.upper) {
return null;
}
scope = scope.upper;
}
return null;
},
};
return utils;
};
/**
* This is a Higher-Order Function that enhances ESLint rules with React component detection
*
* What it does:
* 1. Takes a rule factory function as input
* 2. Returns a new function that creates enhanced ESLint rules
*/
const enhanceRuleWithComponentDetection = (rule) => {
return (context, _node) => {
const sourceCode = context.sourceCode;
const components = new Components();
const utils = createUtils(context, sourceCode);
// Component detection instructions
const detectionInstructions = {
ClassDeclaration(node) {
if (!utils.isES6Component(node)) {
return;
}
components.add(node, 2);
},
ClassProperty(node) {
const parentNode = utils.getParentComponent(node);
if (!parentNode) {
return;
}
components.add(parentNode, 2);
},
ObjectExpression(node) {
if (!utils.isES5Component(node)) {
return;
}
components.add(node, 2);
},
// example: const MyComponent = wrapper(function() { return <JSX />; });
// example 2: const MyComponent = wrapper(function named() { return <JSX />; });
// confidence 1 because we can't be sure it's a component until we see a return statement
// and return statements case is handled separately
FunctionExpression(node) {
const parentNode = utils.getParentComponent(node);
if (!parentNode) {
return;
}
components.add(parentNode, 1);
},
// example: function MyComponent() { return <JSX />; }
// confidence 1 because we can't be sure it's a component until we see a return statement
// and return statements case is handled separately
FunctionDeclaration(node) {
const parentNode = utils.getParentComponent(node);
if (!parentNode) {
return;
}
components.add(parentNode, 1);
},
// example: const MyComponent = wrapper(() => { return <JSX />; });
// example 2: const MyComponent = wrapper(() => <JSX />);
// No need to handle () => {return <JSX />} as it's covered by ReturnStatement
// Hence the node.expression check
ArrowFunctionExpression(node) {
if (node.expression && utils.isArrowFunctionReturningJSXDirectly(node)) {
components.add(node, 2);
}
// Otherwise, we can't be sure it's a component until we see a return statement
else {
components.add(node, 1);
}
},
ThisExpression(node) {
const parentNode = utils.getParentComponent(node);
if (!parentNode || !/Function/.test(parentNode.type)) {
return;
}
// Ban functions with a ThisExpression
components.add(parentNode, 0);
},
// Check all return statements
ReturnStatement(node) {
if (!utils.isJSXReturnStatement(node)) {
return;
}
const parentNode = utils.getParentComponent(node);
if (!parentNode) {
return;
}
components.add(parentNode, 2);
},
};
// Update the provided rule instructions to add the component detection
const ruleInstructions = rule(context, components, utils);
const updatedRuleInstructions = { ...ruleInstructions };
Object.keys(detectionInstructions).forEach((instruction) => {
updatedRuleInstructions[instruction] = (node) => {
detectionInstructions[instruction]?.(node);
return ruleInstructions[instruction]?.(node);
};
});
// Return the updated rule instructions
return updatedRuleInstructions;
};
};
exports.enhanceRuleWithComponentDetection = enhanceRuleWithComponentDetection;
exports.componentsUtils = { enhanceRuleWithComponentDetection: exports.enhanceRuleWithComponentDetection };