From 716457354169ed32d0578bb8e3530c331a35fa1e Mon Sep 17 00:00:00 2001 From: arturovt Date: Sun, 17 May 2026 23:44:30 +0300 Subject: [PATCH] perf(compiler-cli): use BindingType enum check in suffix-not-supported extended diagnostic Replaces the `node.keySpan.toString().startsWith('attr.')` string allocation in the `suffixNotSupported` extended template check with an O(1) `node.type === BindingType.Attribute` enum comparison. The diagnostic message string is also extracted to a module-level constant so it is created once at module load time instead of on every diagnostic emit. Additionally, this change adds missing test coverage for the `.%` and `.em` suffixes, as well as for a plain `attr.` binding without a style suffix. Measured with a 100-iteration microbenchmark before and after the change (MacBook Pro 2018, Intel CPU): ```ts const start = performance.now(); for (let i = 0; i < 100; i++) { new ExtendedTemplateCheckerImpl(templateTypeChecker, program.getTypeChecker(), [suffixNotSupportedFactory], {}).getDiagnosticsForComponent(component); } console.log((performance.now() - start) / 100, 'ms/iter'); ``` Before: `~0.24 ms/iter` After: `~0.14 ms/iter` (~40% faster) --- .../checks/suffix_not_supported/index.ts | 20 +++--- .../suffix_not_supported_spec.ts | 70 ++++++++++++++++++- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts index 265c2d235890..a9e0b424b3ff 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {AST, TmplAstBoundAttribute, TmplAstNode} from '@angular/compiler'; +import {AST, BindingType, TmplAstBoundAttribute, TmplAstNode} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics'; @@ -20,6 +20,11 @@ import { const STYLE_SUFFIXES = ['px', '%', 'em']; +const SUFFIX_ERROR_MSG = formatExtendedError( + ErrorCode.SUFFIX_NOT_SUPPORTED, + `The ${STYLE_SUFFIXES.map((suffix) => `'.${suffix}'`).join(', ')} suffixes are only supported on style bindings`, +); + /** * A check which detects when the `.px`, `.%`, and `.em` suffixes are used with an attribute * binding. These suffixes are only available for style bindings. @@ -35,22 +40,13 @@ class SuffixNotSupportedCheck extends TemplateCheckWithVisitor node.name.endsWith(`.${suffix}`)) ) { return []; } - const diagnostic = ctx.makeTemplateDiagnostic( - node.keySpan, - formatExtendedError( - ErrorCode.SUFFIX_NOT_SUPPORTED, - `The ${STYLE_SUFFIXES.map((suffix) => `'.${suffix}'`).join( - ', ', - )} suffixes are only supported on style bindings`, - ), - ); - return [diagnostic]; + return [ctx.makeTemplateDiagnostic(node.keySpan, SUFFIX_ERROR_MSG)]; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts index 6ac35d83bee5..a47081bc11a2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {DiagnosticCategoryLabel} from '../../../../../core/api'; import ts from 'typescript'; import {ErrorCode, ExtendedTemplateDiagnosticName, ngErrorCode} from '../../../../../diagnostics'; @@ -97,5 +96,74 @@ runInEachFileSystem(() => { const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); + + it('should produce suffix not supported warning for .% suffix', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'TestCmp': `
`}, + source: 'export class TestCmp {}', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.SUFFIX_NOT_SUPPORTED)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe('attr.opacity.%'); + }); + + it('should produce suffix not supported warning for .em suffix', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'TestCmp': `
`}, + source: 'export class TestCmp {}', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.SUFFIX_NOT_SUPPORTED)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe('attr.font-size.em'); + }); + + it('should not produce warning for attr binding without a style suffix', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'TestCmp': `
`}, + source: 'export class TestCmp { x = 1; }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); }); });