diff --git a/packages/eslint-plugin-template/docs/rules/no-call-expression.md b/packages/eslint-plugin-template/docs/rules/no-call-expression.md index 494af1678..643d7b045 100644 --- a/packages/eslint-plugin-template/docs/rules/no-call-expression.md +++ b/packages/eslint-plugin-template/docs/rules/no-call-expression.md @@ -23,7 +23,17 @@ Disallows calling expressions in templates, except for output handlers ## Rule Options -The rule does not have any configuration options. +The rule accepts an options object with the following properties: + +```ts +interface Options { + /** + * Default: `[]` + */ + allowList?: string[]; +} + +```
@@ -391,6 +401,39 @@ The rule does not have any configuration options.
``` +
+ +--- + +
+ +#### Custom Config + +```json +{ + "rules": { + "@angular-eslint/template/no-call-expression": [ + "error", + { + "allowList": [ + "nested", + "getHref" + ] + } + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ obj?.nested() }} {{ obj!.nested() }} +info +``` +
diff --git a/packages/eslint-plugin-template/src/rules/no-call-expression.ts b/packages/eslint-plugin-template/src/rules/no-call-expression.ts index 1910b76fe..53ff1a951 100644 --- a/packages/eslint-plugin-template/src/rules/no-call-expression.ts +++ b/packages/eslint-plugin-template/src/rules/no-call-expression.ts @@ -1,10 +1,14 @@ -import type { Call } from '@angular-eslint/bundled-angular-compiler'; +import type { AST, Call } from '@angular-eslint/bundled-angular-compiler'; import { TmplAstBoundEvent } from '@angular-eslint/bundled-angular-compiler'; import { ensureTemplateParser } from '@angular-eslint/utils'; import { createESLintRule } from '../utils/create-eslint-rule'; import { getNearestNodeFrom } from '../utils/get-nearest-node-from'; -type Options = []; +type Options = [ + { + readonly allowList?: readonly string[]; + }, +]; export type MessageIds = 'noCallExpression'; export const RULE_NAME = 'no-call-expression'; @@ -17,13 +21,25 @@ export default createESLintRule({ 'Disallows calling expressions in templates, except for output handlers', recommended: false, }, - schema: [], + schema: [ + { + additionalProperties: false, + properties: { + allowList: { + items: { type: 'string' }, + type: 'array', + uniqueItems: true, + }, + }, + type: 'object', + }, + ], messages: { noCallExpression: 'Avoid calling expressions in templates', }, }, - defaultOptions: [], - create(context) { + defaultOptions: [{ allowList: [] }], + create(context, [{ allowList }]) { ensureTemplateParser(context); const sourceCode = context.getSourceCode(); @@ -35,6 +51,8 @@ export default createESLintRule({ if (isChildOfBoundEvent) return; + if (isCallNameInAllowList(node.receiver, allowList)) return; + const { sourceSpan: { start, end }, } = node; @@ -53,3 +71,21 @@ export default createESLintRule({ function isBoundEvent(node: unknown): node is TmplAstBoundEvent { return node instanceof TmplAstBoundEvent; } + +function isASTWithName( + ast: AST & { name?: string }, +): ast is AST & { name: string } { + return !!ast.name; +} + +function isCallNameInAllowList( + ast: AST & { name?: string }, + allowList?: readonly string[], +): boolean | undefined { + return ( + allowList && + allowList.length > 0 && + isASTWithName(ast) && + allowList.indexOf(ast.name) > -1 + ); +} diff --git a/packages/eslint-plugin-template/tests/rules/no-call-expression/cases.ts b/packages/eslint-plugin-template/tests/rules/no-call-expression/cases.ts index 66cd0fb96..458a4cf5e 100644 --- a/packages/eslint-plugin-template/tests/rules/no-call-expression/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/no-call-expression/cases.ts @@ -11,6 +11,17 @@ export const valid = [ '
', '
', '
', + { + code: ` + {{ obj?.nested() }} {{ obj!.nested() }} + info + `, + options: [ + { + allowList: ['nested', 'getHref'], + }, + ], + }, ]; export const invalid = [