Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -35,22 +40,13 @@ class SuffixNotSupportedCheck extends TemplateCheckWithVisitor<ErrorCode.SUFFIX_
if (!(node instanceof TmplAstBoundAttribute)) return [];

if (
!node.keySpan.toString().startsWith('attr.') ||
node.type !== BindingType.Attribute ||
!STYLE_SUFFIXES.some((suffix) => 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)];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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': `<div [attr.opacity.%]="50"></div>`},
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': `<div [attr.font-size.em]="1.5"></div>`},
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': `<div [attr.data-value]="x"></div>`},
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);
});
});
});
Loading