@wordpress/eslint-plugin
Version:
ESLint plugin for WordPress development.
157 lines (139 loc) • 4.48 kB
JavaScript
/** @typedef {import('eslint').Scope.Scope} ESLintScope */
/** @typedef {import('eslint').Rule.RuleContext} ESLintRuleContext */
/** @typedef {import('estree').Node} ESTreeNode */
/**
* Mapping of function scope objects to a set of identified JSX identifiers
* within that scope.
*
* @type {WeakMap<ESLintScope,Set<ESTreeNode>>}
*/
const FUNCTION_SCOPE_JSX_IDENTIFIERS = new WeakMap();
/**
* Returns the closest function scope for the current ESLint context object, or
* undefined if it cannot be determined.
*
* @param {ESLintRuleContext} context ESLint context object.
*
* @return {ESLintScope|undefined} Function scope, if known.
*/
function getClosestFunctionScope( context ) {
let functionScope = context.getScope();
while ( functionScope.type !== 'function' && functionScope.upper ) {
functionScope = functionScope.upper;
}
return functionScope;
}
module.exports = /** @type {import('eslint').Rule} */ ( {
meta: {
type: 'problem',
schema: [
{
type: 'object',
properties: {
excludePattern: {
type: 'string',
},
},
additionalProperties: false,
},
],
},
/**
* @param {ESLintRuleContext} context Rule context.
*/
create( context ) {
const options = context.options[ 0 ] || {};
const { excludePattern } = options;
/**
* Given an Espree VariableDeclarator node, returns true if the node
* can be exempted from consideration as unused, or false otherwise. A
* node can be exempt if it destructures to multiple variables, since
* those other variables may be used prior to the return statement. A
* future enhancement could validate that they are in-fact referenced.
*
* @param {Object} node Node to test.
*
* @return {boolean} Whether declarator is emempt from consideration.
*/
function isExemptObjectDestructureDeclarator( node ) {
return (
node.id.type === 'ObjectPattern' &&
node.id.properties.length > 1
);
}
return {
JSXIdentifier( node ) {
// Currently, a scope's variable references does not include JSX
// identifiers. Account for this by visiting JSX identifiers
// first, and tracking them in a map per function scope, which
// is later merged with the known variable references.
const functionScope = getClosestFunctionScope( context );
if ( ! functionScope ) {
return;
}
if ( ! FUNCTION_SCOPE_JSX_IDENTIFIERS.has( functionScope ) ) {
FUNCTION_SCOPE_JSX_IDENTIFIERS.set(
functionScope,
new Set()
);
}
FUNCTION_SCOPE_JSX_IDENTIFIERS.get( functionScope ).add( node );
},
'ReturnStatement:exit'( node ) {
const functionScope = getClosestFunctionScope( context );
if ( ! functionScope ) {
return;
}
for ( const variable of functionScope.variables ) {
const declaratorCandidate = variable.defs.find( ( def ) => {
return (
def.node.type === 'VariableDeclarator' &&
// Allow declarations which are not initialized.
def.node.init &&
// Target function calls as "expensive".
def.node.init.type === 'CallExpression' &&
// Allow unused if part of an object destructuring.
! isExemptObjectDestructureDeclarator( def.node ) &&
// Only target assignments preceding `return`.
def.node.range[ 1 ] < node.range[ 1 ]
);
} );
if ( ! declaratorCandidate ) {
continue;
}
if (
excludePattern !== undefined &&
new RegExp( excludePattern ).test(
declaratorCandidate.node.init.callee.name
)
) {
continue;
}
// The first entry in `references` is the declaration
// itself, which can be ignored.
const identifiers = variable.references
.slice( 1 )
.map( ( reference ) => reference.identifier );
// Merge with any JSX identifiers in scope, if any.
if ( FUNCTION_SCOPE_JSX_IDENTIFIERS.has( functionScope ) ) {
const jsxIdentifiers =
FUNCTION_SCOPE_JSX_IDENTIFIERS.get( functionScope );
identifiers.push( ...jsxIdentifiers );
}
const isUsedBeforeReturn = identifiers.some(
( identifier ) =>
identifier.range[ 1 ] < node.range[ 1 ]
);
if ( isUsedBeforeReturn ) {
continue;
}
context.report(
declaratorCandidate.node,
'Variables should not be assigned until just prior its first reference. ' +
'An early return statement may leave this variable unused.'
);
}
},
};
},
} );