From 563dbd998c86e850b3c4dde4c7cee00d7c9d7581 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:00:58 -0500 Subject: [PATCH] feat(compiler-cli): Adds diagnostic for misconfigured @defer triggers (#64069) Warns when @defer blocks define unreachable or redundant triggers, such as multiple main triggers, ineffective prefetches, or timer delays not scheduled before rendering. PR Close #64069 --- .../reference/extended-diagnostics/NG8021.md | 181 ++++++++++++++++ .../extended-diagnostics/overview.md | 1 + .../public-api/compiler-cli/error_code.api.md | 1 + .../extended_template_diagnostic_name.api.md | 2 + .../src/ngtsc/diagnostics/src/error_code.ts | 7 + .../src/extended_template_diagnostic_name.ts | 1 + .../src/ngtsc/typecheck/extended/BUILD.bazel | 1 + .../BUILD.bazel | 17 ++ .../defer_trigger_misconfiguration/index.ts | 140 +++++++++++++ .../src/ngtsc/typecheck/extended/index.ts | 2 + .../BUILD.bazel | 27 +++ .../defer_trigger_misconfiguration_spec.ts | 197 ++++++++++++++++++ 12 files changed, 577 insertions(+) create mode 100644 adev/src/content/reference/extended-diagnostics/NG8021.md create mode 100644 packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/defer_trigger_misconfiguration_spec.ts diff --git a/adev/src/content/reference/extended-diagnostics/NG8021.md b/adev/src/content/reference/extended-diagnostics/NG8021.md new file mode 100644 index 00000000000..629d34c9cdc --- /dev/null +++ b/adev/src/content/reference/extended-diagnostics/NG8021.md @@ -0,0 +1,181 @@ +# Defer Trigger Misconfiguration + +This diagnostic detects unreachable or redundant triggers in `@defer` blocks. + +```typescript + +import {Component} from '@angular/core'; + +@Component({ + template: ` + @defer (on immediate; on timer(500ms)) { + + } + ` +}) +class MyComponent {} +``` + +## What's wrong with that? + +The diagnostic identifies several problematic patterns in defer trigger configuration that lead to: + +- **Unnecessary code** that never affects behavior +- **Missed optimization opportunities** for better performance +- **Unreachable prefetch triggers** that will never execute + + +## Diagnostic warning cases + +This diagnostic flags the following problematic patterns: + +### `immediate` with prefetch triggers + +**Bad — prefetch never runs** + +```typescript +@Component({ + template: ` + @defer (on immediate; prefetch on idle) { + + } + ` +}) +class MyComponent {} +``` + +**Good — remove redundant prefetch** + +```typescript +@Component({ + template: ` + @defer (on immediate) { + + } + ` +}) +class MyComponent {} +``` + +### Prefetch timer not earlier than main timer + +**Bad — prefetch is later than main** + +```typescript +@Component({ + template: ` + @defer (on timer(100ms); prefetch on timer(3000ms)) { + + } + ` +}) +class MyComponent {} +``` + +**Bad — equal timing provides no benefit** + +```typescript +@Component({ + template: ` + @defer (on timer(500ms); prefetch on timer(500ms)) { + + } + ` +}) +class MyComponent {} +``` + +**Good — prefetch fires earlier** + +```typescript +@Component({ + template: ` + @defer (on timer(1000ms); prefetch on timer(500ms)) { + + } + ` +}) +class MyComponent {} +``` + +### Identical prefetch and main triggers + +**Bad — identical viewport trigger** + +```typescript +@Component({ + template: ` + @defer (on viewport; prefetch on viewport) { + + } + ` +}) +class MyComponent {} +``` + +**Bad — identical interaction target** + +```typescript +@Component({ + template: ` + + @defer (on interaction(loadBtn); prefetch on interaction(loadBtn)) { + + } + ` +}) +class MyComponent {} +``` + +**Good — remove redundant prefetch** + +```typescript +@Component({ + template: ` + + @defer (on interaction(loadBtn)) { + + } + ` +}) +class MyComponent {} +``` + +**Good — use different targets for prefetch and main** + +```typescript +@Component({ + template: ` +
Hover to prefetch
+ + @defer (on interaction(clickBtn); prefetch on hover(hoverArea)) { + + } + ` +}) +class MyComponent {} +``` + + +## Configuration requirements + +[`strictTemplates`](tools/cli/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit. +`deferTriggerMisconfiguration` has no additional requirements beyond `strictTemplates`. + +## What if I can't avoid this? + +This diagnostic can be disabled by editing the project's `tsconfig.json` file: + +```json +{ + "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + "deferTriggerMisconfiguration": "suppress" + } + } + } +} +``` + +See [extended diagnostic configuration](extended-diagnostics#configuration) for more info. diff --git a/adev/src/content/reference/extended-diagnostics/overview.md b/adev/src/content/reference/extended-diagnostics/overview.md index b53c73e56c1..e70a073f7bb 100644 --- a/adev/src/content/reference/extended-diagnostics/overview.md +++ b/adev/src/content/reference/extended-diagnostics/overview.md @@ -25,6 +25,7 @@ Currently, Angular supports the following extended diagnostics: | `NG8115` | [`uninvokedTrackFunction`](extended-diagnostics/NG8115) | | `NG8116` | [`missingStructuralDirective`](extended-diagnostics/NG8116) | | `NG8117` | [`uninvokedFunctionInTextInterpolation`](extended-diagnostics/NG8117) | +| `NG8021` | [`deferTriggerMisconfiguration`](extended-diagnostics/NG8021) | ## Configuration diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md index b12e3875c54..6a40c86a76e 100644 --- a/goldens/public-api/compiler-cli/error_code.api.md +++ b/goldens/public-api/compiler-cli/error_code.api.md @@ -41,6 +41,7 @@ export enum ErrorCode { DECORATOR_UNEXPECTED = 1005, DEFER_IMPLICIT_TRIGGER_INVALID_PLACEHOLDER = 8020, DEFER_IMPLICIT_TRIGGER_MISSING_PLACEHOLDER = 8019, + DEFER_TRIGGER_MISCONFIGURATION = 8021, DEFERRED_DEPENDENCY_IMPORTED_EAGERLY = 8014, DEFERRED_DIRECTIVE_USED_EAGERLY = 8013, DEFERRED_PIPE_USED_EAGERLY = 8012, diff --git a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md index 5650f3d310c..7d3f21c23ba 100644 --- a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md +++ b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md @@ -9,6 +9,8 @@ export enum ExtendedTemplateDiagnosticName { // (undocumented) CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION = "controlFlowPreventingContentProjection", // (undocumented) + DEFER_TRIGGER_MISCONFIGURATION = "deferTriggerMisconfiguration", + // (undocumented) INTERPOLATED_SIGNAL_NOT_INVOKED = "interpolatedSignalNotInvoked", // (undocumented) INVALID_BANANA_IN_BOX = "invalidBananaInBox", diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts index d0a9b07fd42..7c3ffae8909 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts @@ -423,6 +423,13 @@ export enum ErrorCode { */ DEFER_IMPLICIT_TRIGGER_INVALID_PLACEHOLDER = 8020, + /** + * Raised when an `@defer` block defines unreachable or redundant triggers. + * Examples: multiple main triggers, 'on immediate' together with other mains or any prefetch, + * prefetch timer delay that is not earlier than the main timer, or an identical prefetch + */ + DEFER_TRIGGER_MISCONFIGURATION = 8021, + /** * A two way binding in a template has an incorrect syntax, * parentheses outside brackets. For example: diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts index fb5f6f3664f..356339c9280 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts @@ -33,4 +33,5 @@ export enum ExtendedTemplateDiagnosticName { UNUSED_STANDALONE_IMPORTS = 'unusedStandaloneImports', UNPARENTHESIZED_NULLISH_COALESCING = 'unparenthesizedNullishCoalescing', UNINVOKED_FUNCTION_IN_TEXT_INTERPOLATION = 'uninvokedFunctionInTextInterpolation', + DEFER_TRIGGER_MISCONFIGURATION = 'deferTriggerMisconfiguration', } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel index 01b5bddcdb5..ddfeda8ac83 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel @@ -13,6 +13,7 @@ ts_project( "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/typecheck/extended/api", + "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration", "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked", "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box", "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/BUILD.bazel new file mode 100644 index 00000000000..3835d2f5938 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/BUILD.bazel @@ -0,0 +1,17 @@ +load("//tools:defaults.bzl", "ts_project") + +ts_project( + name = "defer_trigger_misconfiguration", + srcs = ["index.ts"], + visibility = [ + "//packages/compiler-cli/src/ngtsc:__subpackages__", + "//packages/compiler-cli/test/ngtsc:__pkg__", + ], + deps = [ + "//:node_modules/typescript", + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/typecheck/api", + "//packages/compiler-cli/src/ngtsc/typecheck/extended/api", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/index.ts new file mode 100644 index 00000000000..c4cb98766d2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration/index.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import ts from 'typescript'; + +import { + TmplAstDeferredBlock, + TmplAstDeferredTrigger, + TmplAstHoverDeferredTrigger, + TmplAstImmediateDeferredTrigger, + TmplAstInteractionDeferredTrigger, + TmplAstTimerDeferredTrigger, + TmplAstViewportDeferredTrigger, +} from '@angular/compiler'; + +import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics'; +import {NgTemplateDiagnostic} from '../../../api'; +import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api'; + +/** + * This check implements warnings for unreachable or redundant @defer triggers. + * Emits ErrorCode.DEFER_TRIGGER_MISCONFIGURATION with messages matching the project's + * expected text. + */ +class DeferTriggerMisconfiguration extends TemplateCheckWithVisitor { + override code = ErrorCode.DEFER_TRIGGER_MISCONFIGURATION as const; + + override visitNode( + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: any, + ): NgTemplateDiagnostic[] { + if (!(node instanceof TmplAstDeferredBlock)) return []; + + const mainKeys = Object.keys(node.triggers) as Array; + const prefetchKeys = Object.keys(node.prefetchTriggers) as Array< + keyof typeof node.prefetchTriggers + >; + + // Gather actual trigger objects for mains and prefetch (only defined ones) + const mains = mainKeys + .map((k) => node.triggers[k]) + .filter((t): t is TmplAstDeferredTrigger => t !== undefined && t !== null); + + const prefetches = prefetchKeys + .map((k) => node.prefetchTriggers[k]) + .filter((t): t is TmplAstDeferredTrigger => t !== undefined && t !== null); + + const diags: NgTemplateDiagnostic[] = []; + + // 'on immediate' dominance + const hasImmediateMain = mains.some((t) => t instanceof TmplAstImmediateDeferredTrigger); + if (hasImmediateMain) { + if (mains.length > 1) { + const msg = `The 'immediate' trigger makes additional triggers redundant.`; + diags.push(ctx.makeTemplateDiagnostic(node.sourceSpan, msg)); + } + if (prefetches.length > 0) { + const msg = `Prefetch triggers have no effect because 'immediate' executes earlier.`; + diags.push(ctx.makeTemplateDiagnostic(node.sourceSpan, msg)); + } + } + + // If there is exactly one main and at least one prefetch, compare them. + if (mains.length === 1 && prefetches.length > 0) { + const main = mains[0]; + + for (const pre of prefetches) { + // Timer vs Timer: warn when prefetch delay >= main delay + const isTimerTriggger = + main instanceof TmplAstTimerDeferredTrigger && pre instanceof TmplAstTimerDeferredTrigger; + if (isTimerTriggger) { + const mainDelay = main.delay; + const preDelay = pre.delay; + if (preDelay >= mainDelay) { + const msg = `The Prefetch 'timer(${preDelay}ms)' is not scheduled before the main 'timer(${mainDelay}ms)', so it won’t run prior to rendering. Lower the prefetch delay or remove it.`; + diags.push(ctx.makeTemplateDiagnostic(pre.sourceSpan ?? node.sourceSpan, msg)); + } + } + + // Reference-based triggers (hover/interaction/viewport): only warn if both + // have a reference and the references are identical. If references differ + // (or one is missing), the prefetch targets a different element and + // provides potential value. + + const isHoverTrigger = + main instanceof TmplAstHoverDeferredTrigger && pre instanceof TmplAstHoverDeferredTrigger; + + const isInteractionTrigger = + main instanceof TmplAstInteractionDeferredTrigger && + pre instanceof TmplAstInteractionDeferredTrigger; + + const isViewportTrigger = + main instanceof TmplAstViewportDeferredTrigger && + pre instanceof TmplAstViewportDeferredTrigger; + + if (isHoverTrigger || isInteractionTrigger || isViewportTrigger) { + const mainRef = main.reference; + const preRef = pre.reference; + if (mainRef && preRef && mainRef === preRef) { + const kindName = main.constructor.name.replace('DeferredTrigger', '').toLowerCase(); + const msg = `Prefetch '${kindName}' matches the main trigger and provides no benefit. Remove the prefetch modifier.`; + diags.push(ctx.makeTemplateDiagnostic(pre.sourceSpan ?? node.sourceSpan, msg)); + } + // otherwise, different references or missing reference => no warning + continue; + } + + // Syntactic identical: same class for immediate/idle/never etc. (timers handled above) + if ( + main.constructor === pre.constructor && + !(main instanceof TmplAstTimerDeferredTrigger) + ) { + const kind = + main instanceof TmplAstImmediateDeferredTrigger + ? 'immediate' + : main.constructor.name.replace('DeferredTrigger', '').toLowerCase(); + const msg = `Prefetch '${kind}' matches the main trigger and provides no benefit. Remove the prefetch modifier.`; + diags.push(ctx.makeTemplateDiagnostic(pre.sourceSpan ?? node.sourceSpan, msg)); + } + } + } + + return diags; + } +} + +export const factory: TemplateCheckFactory< + ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, + ExtendedTemplateDiagnosticName.DEFER_TRIGGER_MISCONFIGURATION +> = { + code: ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, + name: ExtendedTemplateDiagnosticName.DEFER_TRIGGER_MISCONFIGURATION, + create: () => new DeferTriggerMisconfiguration(), +}; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts index 8cac32402d3..a6676e123f1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts @@ -24,6 +24,7 @@ import {factory as unparenthesizedNullishCoalescingFactory} from './checks/unpar import {factory as unusedLetDeclarationFactory} from './checks/unused_let_declaration'; import {factory as uninvokedTrackFunctionFactory} from './checks/uninvoked_track_function'; import {factory as uninvokedFunctionInTextInterpolationFactory} from './checks/uninvoked_function_in_text_interpolation'; +import {factory as deferTriggerMisconfigurationFactory} from './checks/defer_trigger_misconfiguration'; export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker'; @@ -46,6 +47,7 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory< unparenthesizedNullishCoalescingFactory, uninvokedTrackFunctionFactory, uninvokedFunctionInTextInterpolationFactory, + deferTriggerMisconfigurationFactory, ]; export const SUPPORTED_DIAGNOSTIC_NAMES = new Set([ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/BUILD.bazel new file mode 100644 index 00000000000..31353abe7e7 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/BUILD.bazel @@ -0,0 +1,27 @@ +load("//tools:defaults.bzl", "jasmine_test", "ts_project") + +ts_project( + name = "test_lib", + testonly = True, + srcs = ["defer_trigger_misconfiguration_spec.ts"], + deps = [ + "//:node_modules/typescript", + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/core:api", + "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/src/ngtsc/typecheck/extended", + "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/defer_trigger_misconfiguration", + "//packages/compiler-cli/src/ngtsc/typecheck/testing", + ], +) + +jasmine_test( + name = "test", + data = [ + ":test_lib", + "//packages/core:npm_package", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/defer_trigger_misconfiguration_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/defer_trigger_misconfiguration_spec.ts new file mode 100644 index 00000000000..fc1f02d83e5 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/defer_trigger_misconfiguration/defer_trigger_misconfiguration_spec.ts @@ -0,0 +1,197 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import ts from 'typescript'; +import {ErrorCode, ExtendedTemplateDiagnosticName, ngErrorCode} from '../../../../../diagnostics'; +import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system'; +import {runInEachFileSystem} from '../../../../../file_system/testing'; +import {getClass, setup} from '../../../../testing'; +import {factory as deferTriggerMisconfigurationFactory} from '../../../checks/defer_trigger_misconfiguration'; +import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker'; + +runInEachFileSystem(() => { + describe('DeferTriggerMisconfiguration', () => { + it('binds the error code to its extended template diagnostic name', () => { + expect(deferTriggerMisconfigurationFactory.code).toBe( + ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, + ); + expect(deferTriggerMisconfigurationFactory.name).toBe( + ExtendedTemplateDiagnosticName.DEFER_TRIGGER_MISCONFIGURATION, + ); + }); + + it('should emit when on immediate coexists with other mains', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on immediate; on timer(100ms) ; on viewport(ref) ) {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + 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.DEFER_TRIGGER_MISCONFIGURATION)); + }); + + it('should emit when on immediate coexists with prefetch', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on immediate; prefetch on viewport ) {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + 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.DEFER_TRIGGER_MISCONFIGURATION)); + }); + + it('should emit when prefetch timer >= main timer', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on timer(1s); prefetch on timer(2000ms)) {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + 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.DEFER_TRIGGER_MISCONFIGURATION)); + }); + + it('should emit when prefetch identical to main viewport/interaction/hover', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on viewport(ref); prefetch on viewport(ref)) {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + 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.DEFER_TRIGGER_MISCONFIGURATION)); + }); + + it('should not emit for valid prefetch earlier than main', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on timer(1s); prefetch on timer(500ms)) {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + + it('should not emit when main is viewport(ref) and prefetch is viewport without reference', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on viewport(ref); prefetch on viewport) {
} @placeholder {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + + it('should not emit when main is interaction(refA) and prefetch is interaction(refB)', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `@defer (on interaction(refA); prefetch on interaction(refB)) {
} @placeholder {
}`, + }, + source: 'export class TestCmp { }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [deferTriggerMisconfigurationFactory], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + }); +});