From 7cf8cdd2b2db2df3063c9a7c1038a0982fc3fb0d Mon Sep 17 00:00:00 2001 From: Sandi Barr Date: Sun, 20 Nov 2022 09:18:26 -0600 Subject: [PATCH 1/2] feat(eslint-plugin-template): [no-call-expression] add allowList option --- .../docs/rules/no-call-expression.md | 45 ++++++++++++++++++- .../src/rules/no-call-expression.ts | 41 ++++++++++++++--- .../tests/rules/no-call-expression/cases.ts | 11 +++++ 3 files changed, 91 insertions(+), 6 deletions(-) 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..f22cf99ae 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,15 @@ export default createESLintRule({ if (isChildOfBoundEvent) return; + if ( + allowList && + allowList.length && + isASTWithName(node.receiver) && + allowList.indexOf(node.receiver.name) > -1 + ) { + return; + } + const { sourceSpan: { start, end }, } = node; @@ -53,3 +78,9 @@ 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; +} 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 = [ From f50b11a3c96e2c7dc3a886b84d0748e26eb9514d Mon Sep 17 00:00:00 2001 From: Sandi Barr Date: Mon, 21 Nov 2022 11:51:36 -0600 Subject: [PATCH 2/2] feat(eslint-plugin-template): [no-call-expression] extract check to isCallNameInAllowList() --- .../src/rules/no-call-expression.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 f22cf99ae..53ff1a951 100644 --- a/packages/eslint-plugin-template/src/rules/no-call-expression.ts +++ b/packages/eslint-plugin-template/src/rules/no-call-expression.ts @@ -51,14 +51,7 @@ export default createESLintRule({ if (isChildOfBoundEvent) return; - if ( - allowList && - allowList.length && - isASTWithName(node.receiver) && - allowList.indexOf(node.receiver.name) > -1 - ) { - return; - } + if (isCallNameInAllowList(node.receiver, allowList)) return; const { sourceSpan: { start, end }, @@ -84,3 +77,15 @@ function isASTWithName( ): 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 + ); +}