Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
| [template-no-dynamic-subexpression-invocations](docs/rules/template-no-dynamic-subexpression-invocations.md) | disallow dynamic subexpression invocations | | | |
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
| [template-no-implicit-this](docs/rules/template-no-implicit-this.md) | require explicit `this` in property access | | | |
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
Expand Down
62 changes: 62 additions & 0 deletions docs/rules/template-no-dynamic-subexpression-invocations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ember/template-no-dynamic-subexpression-invocations

<!-- end auto-generated rule header -->

Disallow dynamic helper invocations.

Dynamic helper invocations (where the helper name comes from a property or argument) make code harder to understand and can have performance implications. Use explicit helper names instead.

## Rule Details

This rule disallows invoking helpers dynamically using `this` or `@` properties.

## Examples

### Incorrect ❌

```gjs
<template>
{{(this.helper "arg")}}
</template>
```

```gjs
<template>
{{(@helperName "value")}}
</template>
```

```gjs
<template>
{{this.formatter this.data}}
</template>
```

### Correct ✅

```gjs
<template>
{{format-date this.date}}
</template>
```

```gjs
<template>
{{(upper-case this.name)}}
</template>
```

```gjs
<template>
{{this.formattedData}}
</template>
```

## Related Rules

- [template-no-implicit-this](./template-no-implicit-this.md)

## References

- [Ember Guides - Template Helpers](https://guides.emberjs.com/release/components/helper-functions/)
- [eslint-plugin-ember template-no-dynamic-subexpression-invocations](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-dynamic-subexpression-invocations.md)
157 changes: 157 additions & 0 deletions lib/rules/template-no-dynamic-subexpression-invocations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
function isInAttrPosition(node) {
let p = node.parent;
while (p) {
if (p.type === 'GlimmerAttrNode') {
return true;
}
if (p.type === 'GlimmerConcatStatement') {
p = p.parent;
continue;
}
if (
p.type === 'GlimmerElementNode' ||
p.type === 'GlimmerTemplate' ||
p.type === 'GlimmerBlockStatement' ||
p.type === 'GlimmerBlock'
) {
return false;
}
p = p.parent;
}
return false;
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow dynamic subexpression invocations',
category: 'Best Practices',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-dynamic-subexpression-invocations.md',
templateMode: 'both',
},
fixable: null,
schema: [],
messages: {
noDynamicSubexpressionInvocations:
'Do not use dynamic helper invocations. Use explicit helper names instead.',
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/no-dynamic-subexpression-invocations.js',
docs: 'docs/rule/no-dynamic-subexpression-invocations.md',
tests: 'test/unit/rules/no-dynamic-subexpression-invocations-test.js',
},
},

create(context) {
const localScopes = [];

function pushLocals(params) {
localScopes.push(new Set(params || []));
}

function popLocals() {
localScopes.pop();
}

function isLocal(name) {
for (const scope of localScopes) {
if (scope.has(name)) {
return true;
}
}
return false;
}

function isDynamicPath(path) {
if (!path || path.type !== 'GlimmerPathExpression') {
return false;
}
if (path.head?.type === 'AtHead') {
return true;
}
if (path.head?.type === 'ThisHead') {
return true;
}
if (path.original && path.original.includes('.')) {
return true;
}
if (path.original && isLocal(path.original)) {
return true;
}
return false;
}

return {
GlimmerBlockStatement(node) {
if (node.program && node.program.blockParams) {
pushLocals(node.program.blockParams);
}
},
'GlimmerBlockStatement:exit'(node) {
if (node.program && node.program.blockParams) {
popLocals();
}
},

GlimmerElementNode(node) {
if (node.blockParams && node.blockParams.length > 0) {
pushLocals(node.blockParams);
}
},
'GlimmerElementNode:exit'(node) {
if (node.blockParams && node.blockParams.length > 0) {
popLocals();
}
},

GlimmerSubExpression(node) {
if (node.path && node.path.type === 'GlimmerPathExpression' && isDynamicPath(node.path)) {
context.report({
node,
messageId: 'noDynamicSubexpressionInvocations',
});
}
},

GlimmerElementModifierStatement(node) {
if (node.path && node.path.type === 'GlimmerPathExpression' && isDynamicPath(node.path)) {
context.report({
node,
messageId: 'noDynamicSubexpressionInvocations',
});
}
},

GlimmerMustacheStatement(node) {
if (node.path && node.path.type === 'GlimmerPathExpression') {
const inAttr = isInAttrPosition(node);
const hasArgs =
(node.params && node.params.length > 0) ||
(node.hash && node.hash.pairs && node.hash.pairs.length > 0);

if (inAttr && isDynamicPath(node.path) && hasArgs) {
// In attribute context, flag dynamic paths with arguments
context.report({
node,
messageId: 'noDynamicSubexpressionInvocations',
});
return;
}

if (!inAttr && hasArgs) {
// In body context, only flag this.* paths (not @args)
if (node.path.head?.type === 'ThisHead') {
context.report({
node,
messageId: 'noDynamicSubexpressionInvocations',
});
}
}
}
},
};
},
};
Loading
Loading