eslint-plugin-ember
Version:
ESLint plugin for Ember.js apps
209 lines (188 loc) • 5.73 kB
JavaScript
function collectChildNodes(n) {
const children = [];
if (n.program) {
children.push(n.program);
}
if (n.inverse) {
children.push(n.inverse);
}
if (n.params) {
children.push(...n.params);
}
if (n.hash?.pairs) {
children.push(...n.hash.pairs.map((p) => p.value));
}
if (n.body) {
children.push(...n.body);
}
if (n.path) {
children.push(n.path);
}
if (n.attributes) {
children.push(...n.attributes.map((a) => a.value));
}
if (n.children) {
children.push(...n.children);
}
if (n.modifiers) {
children.push(...n.modifiers);
}
// GlimmerPathExpression also has 'parts', so make sure we're not treating
// concat'd path string parts as AST nodes.
if (n.type === 'GlimmerConcatStatement' && n.parts) {
children.push(...n.parts);
}
return children;
}
function markParamIfUsed(name, blockParams, usedParams, shadowedParams) {
const firstPart = name.split('.')[0];
if (blockParams.includes(firstPart) && !shadowedParams.has(firstPart)) {
usedParams.add(firstPart);
}
}
function isPartialStatement(n) {
return (
(n.type === 'GlimmerMustacheStatement' || n.type === 'GlimmerBlockStatement') &&
n.path?.original === 'partial'
);
}
function buildShadowedSet(shadowedParams, innerBlockParams, outerBlockParams) {
const newShadowed = new Set(shadowedParams);
for (const p of innerBlockParams) {
if (outerBlockParams.includes(p)) {
newShadowed.add(p);
}
}
return newShadowed;
}
function checkBlockParts(n, blockParams, usedParams, shadowedParams, newShadowed, checkNodeFn) {
// Check the path/params of the block statement itself with current scope
if (n.path) {
checkNodeFn(n.path, shadowedParams);
}
if (n.params) {
for (const param of n.params) {
checkNodeFn(param, shadowedParams);
}
}
if (n.hash?.pairs) {
for (const pair of n.hash.pairs) {
checkNodeFn(pair.value, shadowedParams);
}
}
// Check the program body with the updated shadowed set
if (n.program) {
checkNodeFn(n.program, newShadowed);
}
if (n.inverse) {
checkNodeFn(n.inverse, newShadowed);
}
}
/**
* Scan child nodes for usage of blockParams, then report the first unused
* trailing param. Shared by both GlimmerBlockStatement and GlimmerElementNode.
*/
function checkUnusedBlockParams(context, node, blockParams, startNodes) {
const usedParams = new Set();
function checkNode(n, shadowedParams) {
if (!n) {
return;
}
if (n.type === 'GlimmerPathExpression') {
markParamIfUsed(n.original, blockParams, usedParams, shadowedParams);
}
if (n.type === 'GlimmerElementNode') {
markParamIfUsed(n.tag, blockParams, usedParams, shadowedParams);
}
if (isPartialStatement(n)) {
for (const p of blockParams) {
if (!shadowedParams.has(p)) {
usedParams.add(p);
}
}
}
// Nested block with its own blockParams — shadow them
if (n.type === 'GlimmerBlockStatement' && n.program?.blockParams?.length > 0) {
const newShadowed = buildShadowedSet(shadowedParams, n.program.blockParams, blockParams);
checkBlockParts(n, blockParams, usedParams, shadowedParams, newShadowed, checkNode);
return;
}
// Nested element with block params (e.g. <Component as |x|>) — shadow them
if (n.type === 'GlimmerElementNode' && n.blockParams?.length > 0) {
const newShadowed = buildShadowedSet(shadowedParams, n.blockParams, blockParams);
if (n.attributes) {
for (const attr of n.attributes) {
checkNode(attr.value, shadowedParams);
}
}
if (n.children) {
for (const child of n.children) {
checkNode(child, newShadowed);
}
}
return;
}
for (const child of collectChildNodes(n)) {
checkNode(child, shadowedParams);
}
}
for (const startNode of startNodes) {
checkNode(startNode, new Set());
}
// Find the last index of a used param
let lastUsedIndex = -1;
for (let i = blockParams.length - 1; i >= 0; i--) {
if (usedParams.has(blockParams[i])) {
lastUsedIndex = i;
break;
}
}
// Only report trailing unused params (after the last used one)
const unusedTrailing = blockParams.slice(lastUsedIndex + 1);
const firstUnusedTrailing = unusedTrailing[0];
if (firstUnusedTrailing) {
context.report({
node,
messageId: 'unusedBlockParam',
data: { param: firstUnusedTrailing },
});
}
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow unused block parameters in templates',
category: 'Best Practices',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unused-block-params.md',
templateMode: 'both',
},
schema: [],
messages: {
unusedBlockParam: "'{{param}}' is defined but never used",
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/no-unused-block-params.js',
docs: 'docs/rule/no-unused-block-params.md',
tests: 'test/unit/rules/no-unused-block-params-test.js',
},
},
create(context) {
return {
GlimmerBlockStatement(node) {
const blockParams = node.program?.blockParams || [];
if (blockParams.length > 0) {
checkUnusedBlockParams(context, node, blockParams, [node.program]);
}
},
GlimmerElementNode(node) {
const blockParams = node.blockParams || [];
if (blockParams.length > 0) {
checkUnusedBlockParams(context, node, blockParams, node.children || []);
}
},
};
},
};