UNPKG

eslint-plugin-ember

Version:
401 lines (359 loc) 11.5 kB
/* eslint-disable unicorn/consistent-function-scoping, unicorn/prefer-switch, curly */ const COMPONENT_HELPER_NAME = 'component'; function dasherize(str) { return str .split('::') .map((segment) => segment .replaceAll(/([A-Z])/g, '-$1') .toLowerCase() .replace(/^-/, '') ) .join('/'); } function parseConfig(config) { // If config is not provided, disable the rule if (config === false || config === undefined) { return false; } // If it's true, use empty array if (config === true) { return []; } // If it's an array, validate it if (Array.isArray(config)) { return config; } return false; } /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', docs: { description: 'disallow certain components, helpers or modifiers from being used', category: 'Best Practices', recommended: false, url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-restricted-invocations.md', templateMode: 'both', }, fixable: null, schema: [ { oneOf: [ { type: 'array', items: { oneOf: [ { type: 'string', }, { type: 'object', properties: { names: { type: 'array', items: { type: 'string', }, }, message: { type: 'string', }, }, required: ['names', 'message'], additionalProperties: false, }, ], }, }, ], }, ], messages: {}, originallyFrom: { name: 'ember-template-lint', rule: 'lib/rules/no-restricted-invocations.js', docs: 'docs/rule/no-restricted-invocations.md', tests: 'test/unit/rules/no-restricted-invocations-test.js', }, }, create(context) { const config = parseConfig(context.options[0]); if (config === false) { return {}; } const sourceCode = context.sourceCode; // Track block params in a scope stack so yielded names are not flagged. const blockParamScopes = []; function pushBlockParams(params) { blockParamScopes.push(new Set(params || [])); } function popBlockParams() { blockParamScopes.pop(); } function isBlockParam(name) { for (const scope of blockParamScopes) { if (scope.has(name)) { return true; } } return false; } /** * In gjs/gts, check whether a name resolves to a JS-scope variable * (import, const, let, function param, etc.). If it does, it's a local * binding and should be exempt from restriction checks — same as block params. */ function isJsScopeVariable(node) { if (!sourceCode) return false; try { if (node.type === 'GlimmerElementNode') { // Element nodes use parts[0] for scope lookup, and need parent scope if (!node.parts || !node.parts[0]) return false; const scope = sourceCode.getScope(node.parent); const ref = scope.references.find((r) => r.identifier === node.parts[0]); // Only exempt if the reference actually resolves to a JS variable definition return ( ref !== null && ref !== undefined && ref.resolved !== null && ref.resolved !== undefined ); } // For mustache/block/sub/modifier statements, check the path's head if (node.path && node.path.head) { const scope = sourceCode.getScope(node); const ref = scope.references.find((r) => r.identifier === node.path.head); return ( ref !== null && ref !== undefined && ref.resolved !== null && ref.resolved !== undefined ); } } catch { // sourceCode.getScope may not be available in .hbs-only mode; ignore. } return false; } function isRestricted(name) { for (const item of config) { if (typeof item === 'string') { if (item === name) { return { restricted: true, message: null }; } } else if (item.names && item.names.includes(name)) { return { restricted: true, message: item.message }; } } return { restricted: false }; } function getComponentOrHelperName(node) { if (node.type === 'GlimmerElementNode') { // Convert angle-bracket names to kebab-case return dasherize(node.tag); } if (node.type === 'GlimmerMustacheStatement' || node.type === 'GlimmerBlockStatement') { // Check if it's the component helper if (node.path.original === COMPONENT_HELPER_NAME && node.params && node.params[0]) { // component helper with first param if (node.params[0].type === 'GlimmerStringLiteral') { return node.params[0].value; } } return node.path.original; } if (node.type === 'GlimmerModifierStatement') { return node.path.original; } if (node.type === 'GlimmerSubExpression') { if (node.path.original === COMPONENT_HELPER_NAME && node.params && node.params[0]) { if (node.params[0].type === 'GlimmerStringLiteral') { return node.params[0].value; } } return node.path.original; } return null; } function getNodeName(node) { switch (node.type) { case 'GlimmerElementNode': { return `<${node.tag} />`; } case 'GlimmerMustacheStatement': { if ( node.path.original === COMPONENT_HELPER_NAME && node.params?.[0]?.type === 'GlimmerStringLiteral' ) { return `{{component "${node.params[0].value}"}}`; } return `{{${node.path.original}}}`; } case 'GlimmerBlockStatement': { if ( node.path.original === COMPONENT_HELPER_NAME && node.params?.[0]?.type === 'GlimmerStringLiteral' ) { return `{{#component "${node.params[0].value}"}}`; } return `{{#${node.path.original}}}`; } case 'GlimmerModifierStatement': { return `{{${node.path.original}}}`; } case 'GlimmerSubExpression': { if ( node.path.original === COMPONENT_HELPER_NAME && node.params?.[0]?.type === 'GlimmerStringLiteral' ) { return `(component "${node.params[0].value}")`; } return `(${node.path.original})`; } // No default } return ''; } function checkElementModifiers(node) { if (!node.modifiers) { return; } for (const modifier of node.modifiers) { const modName = modifier.path && modifier.path.type === 'GlimmerPathExpression' && modifier.path.original; if (!modName) continue; if (isBlockParam(modName)) continue; if (isJsScopeVariable(modifier)) continue; const modResult = isRestricted(modName); if (modResult.restricted) { context.report({ node: modifier, message: modResult.message || `Cannot use disallowed helper, component or modifier '{{${modName}}}'`, }); } } } function trackBlockParams(node) { if (node.blockParams && node.blockParams.length > 0) { pushBlockParams(node.blockParams); } } return { GlimmerElementNode(node) { // For element nodes, check the raw tag against block params before dasherizing. if (node.tag && isBlockParam(node.tag)) { trackBlockParams(node); return; } // In gjs/gts, skip if the tag resolves to a JS-scope variable (import, const, etc.) if (isJsScopeVariable(node)) { trackBlockParams(node); return; } const name = getComponentOrHelperName(node); if (name && !isBlockParam(name)) { const result = isRestricted(name); if (result.restricted) { context.report({ node, message: result.message || `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`, }); } } trackBlockParams(node); checkElementModifiers(node); }, 'GlimmerElementNode:exit'(node) { if (node.blockParams && node.blockParams.length > 0) { popBlockParams(); } }, GlimmerMustacheStatement(node) { const name = getComponentOrHelperName(node); if (!name) { return; } if (isBlockParam(name)) { return; } if (isJsScopeVariable(node)) { return; } const result = isRestricted(name); if (result.restricted) { context.report({ node, message: result.message || `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`, }); } }, GlimmerBlockStatement(node) { const name = getComponentOrHelperName(node); if (name && !isBlockParam(name) && !isJsScopeVariable(node)) { const result = isRestricted(name); if (result.restricted) { context.report({ node, message: result.message || `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`, }); } } // Track block params (e.g. {{#each items as |item|}}). if (node.program && node.program.blockParams) { pushBlockParams(node.program.blockParams); } }, 'GlimmerBlockStatement:exit'(node) { if (node.program && node.program.blockParams) { popBlockParams(); } }, GlimmerModifierStatement(node) { const name = getComponentOrHelperName(node); if (!name) { return; } if (isBlockParam(name)) { return; } if (isJsScopeVariable(node)) { return; } const result = isRestricted(name); if (result.restricted) { context.report({ node, message: result.message || `Cannot use disallowed helper, component or modifier '{{${name}}}'`, }); } }, GlimmerSubExpression(node) { const name = getComponentOrHelperName(node); if (!name) { return; } if (isBlockParam(name)) { return; } if (isJsScopeVariable(node)) { return; } const result = isRestricted(name); if (result.restricted) { context.report({ node, message: result.message || `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`, }); } }, }; }, }; /* eslint-enable unicorn/consistent-function-scoping, unicorn/prefer-switch, curly */