Skip to content
Merged
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
@@ -0,0 +1,34 @@
/**
* @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 {VERSION} from '@angular/compiler';

import {ErrorCode} from '../../../diagnostics';

/**
* Base URL for the extended error details page.
*
* Keep the files below in full sync:
* - packages/compiler-cli/src/ngtsc/diagnostics/src/error_details_base_url.ts
* - packages/core/src/error_details_base_url.ts
*/
export const EXTENDED_ERROR_DETAILS_PAGE_BASE_URL: string = (() => {
const versionSubDomain = VERSION.major !== '0' ? `v${VERSION.major}.` : '';
return `https://${versionSubDomain}angular.dev/extended-diagnostics`;
})();

export function formatExtendedError(code: ErrorCode, message: null | false | string): string {
// Note: Runtime error codes are prefixed with 0 (e.g., NG0100-999) while compiler errors
// use plain numbers (e.g., NG1001), keeping them distinct despite numerical overlap.
const fullCode = `NG${Math.abs(code)}`;
const errorMessage = `${fullCode}${message ? ': ' + message : ''}`;
const addPeriodSeparator = !errorMessage.match(/[.,;!?\n]$/);
const separator = addPeriodSeparator ? '.' : '';

return `${errorMessage}${separator} Find more at ${EXTENDED_ERROR_DETAILS_PAGE_BASE_URL}/${fullCode}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*/

export * from './api';
export * from './format-extended-error';
export * from './extended_template_checker';
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
formatExtendedError,
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
} from '../../api';

/**
* This check implements warnings for unreachable or redundant @defer triggers.
Expand Down Expand Up @@ -58,11 +63,21 @@ class DeferTriggerMisconfiguration extends TemplateCheckWithVisitor<ErrorCode.DE
if (hasImmediateMain) {
if (mains.length > 1) {
const msg = `The 'immediate' trigger makes additional triggers redundant.`;
diags.push(ctx.makeTemplateDiagnostic(node.sourceSpan, msg));
diags.push(
ctx.makeTemplateDiagnostic(
node.sourceSpan,
formatExtendedError(ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, msg),
),
);
}
if (prefetches.length > 0) {
const msg = `Prefetch triggers have no effect because 'immediate' executes earlier.`;
diags.push(ctx.makeTemplateDiagnostic(node.sourceSpan, msg));
diags.push(
ctx.makeTemplateDiagnostic(
node.sourceSpan,
formatExtendedError(ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, msg),
),
);
}
}

Expand All @@ -79,7 +94,12 @@ class DeferTriggerMisconfiguration extends TemplateCheckWithVisitor<ErrorCode.DE
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));
diags.push(
ctx.makeTemplateDiagnostic(
pre.sourceSpan ?? node.sourceSpan,
formatExtendedError(ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, msg),
),
);
}
}

Expand All @@ -105,7 +125,12 @@ class DeferTriggerMisconfiguration extends TemplateCheckWithVisitor<ErrorCode.DE
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));
diags.push(
ctx.makeTemplateDiagnostic(
pre.sourceSpan ?? node.sourceSpan,
formatExtendedError(ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, msg),
),
);
}
// otherwise, different references or missing reference => no warning
continue;
Expand All @@ -121,7 +146,12 @@ class DeferTriggerMisconfiguration extends TemplateCheckWithVisitor<ErrorCode.DE
? '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));
diags.push(
ctx.makeTemplateDiagnostic(
pre.sourceSpan ?? node.sourceSpan,
formatExtendedError(ErrorCode.DEFER_TRIGGER_MISCONFIGURATION, msg),
),
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import ts from 'typescript';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic, SymbolKind, TypeCheckableDirectiveMeta} from '../../../api';
import {isSignalReference} from '../../../src/symbol_util';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/** Names of known signal instance properties. */
const SIGNAL_INSTANCE_PROPERTIES = new Set(['set', 'update', 'asReadonly']);
Expand Down Expand Up @@ -169,7 +174,12 @@ function buildDiagnosticForSignal(
const templateMapping = ctx.templateTypeChecker.getSourceMappingAtTcbLocation(
symbol.tcbLocation,
)!;
const errorString = `${node.name} is a function and should be invoked: ${node.name}()`;

const errorString = formatExtendedError(
ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
`${node.name} is a function and should be invoked: ${node.name}()}`,
);

const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString);
return [diagnostic];
}
Expand All @@ -192,9 +202,13 @@ function buildDiagnosticForSignal(
symbolOfReceiver.tcbLocation,
)!;

const errorString = `${
(node.receiver as PropertyRead).name
} is a function and should be invoked: ${(node.receiver as PropertyRead).name}()`;
const errorString = formatExtendedError(
ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
`${
(node.receiver as PropertyRead).name
} is a function and should be invoked: ${(node.receiver as PropertyRead).name}()`,
);

const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import {AST, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
import ts from 'typescript';

import {
ErrorCode,
ExtendedTemplateDiagnosticName,
DOC_PAGE_BASE_URL,
} from '../../../../diagnostics';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* Ensures the two-way binding syntax is correct.
Expand All @@ -39,8 +40,10 @@ class InvalidBananaInBoxCheck extends TemplateCheckWithVisitor<ErrorCode.INVALID
const expectedBoundSyntax = boundSyntax.replace(`(${name})`, `[(${name.slice(1, -1)})]`);
const diagnostic = ctx.makeTemplateDiagnostic(
node.sourceSpan,
`In the two-way binding syntax the parentheses should be inside the brackets, ex. '${expectedBoundSyntax}'.
Find more at ${DOC_PAGE_BASE_URL}/guide/templates/two-way-binding`,
formatExtendedError(
ErrorCode.INVALID_BANANA_IN_BOX,
`In the two-way binding syntax the parentheses should be inside the brackets, ex. '${expectedBoundSyntax}'`,
),
);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import ts from 'typescript';
import {NgCompilerOptions} from '../../../../core/api';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* The list of known control flow directives present in the `CommonModule`,
Expand Down Expand Up @@ -74,12 +79,15 @@ class MissingControlFlowDirectiveCheck extends TemplateCheckWithVisitor<ErrorCod

const sourceSpan = controlFlowAttr.keySpan || controlFlowAttr.sourceSpan;
const directiveAndBuiltIn = KNOWN_CONTROL_FLOW_DIRECTIVES.get(controlFlowAttr.name);
const errorMessage =
const errorMessage = formatExtendedError(
ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE,
`The \`*${controlFlowAttr.name}\` directive was used in the template, ` +
`but neither the \`${directiveAndBuiltIn?.directive}\` directive nor the \`CommonModule\` was imported. ` +
`Use Angular's built-in control flow ${directiveAndBuiltIn?.builtIn} or ` +
`make sure that either the \`${directiveAndBuiltIn?.directive}\` directive or the \`CommonModule\` ` +
`is included in the \`@Component.imports\` array of this component.`;
`but neither the \`${directiveAndBuiltIn?.directive}\` directive nor the \`CommonModule\` was imported. ` +
`Use Angular's built-in control flow ${directiveAndBuiltIn?.builtIn} or ` +
`make sure that either the \`${directiveAndBuiltIn?.directive}\` directive or the \`CommonModule\` ` +
`is included in the \`@Component.imports\` array of this component.`,
);

const diagnostic = ctx.makeTemplateDiagnostic(sourceSpan, errorMessage);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* Ensures a user doesn't forget to omit `let` when using ngfor.
Expand Down Expand Up @@ -41,7 +46,12 @@ class MissingNgForOfLetCheck extends TemplateCheckWithVisitor<ErrorCode.MISSING_
if (node.variables.length > 0) {
return [];
}
const errorString = 'Your ngFor is missing a value. Did you forget to add the `let` keyword?';

const errorString = formatExtendedError(
ErrorCode.MISSING_NGFOROF_LET,
`Your ngFor is missing a value. Did you forget to add the \`let\` keyword?`,
);

const diagnostic = ctx.makeTemplateDiagnostic(attr.sourceSpan, errorString);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* The list of known control flow directives present in the `CommonModule`.
Expand Down Expand Up @@ -76,10 +81,13 @@ class MissingStructuralDirectiveCheck extends TemplateCheckWithVisitor<ErrorCode
if (hasStructuralDirective) return [];

const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan;
const errorMessage =
const errorMessage = formatExtendedError(
ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
`A structural directive \`${customStructuralDirective.name}\` was used in the template ` +
`without a corresponding import in the component. ` +
`Make sure that the directive is included in the \`@Component.imports\` array of this component.`;
`without a corresponding import in the component. ` +
`Make sure that the directive is included in the \`@Component.imports\` array of this component.`,
);

return [ctx.makeTemplateDiagnostic(sourceSpan, errorMessage)];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import ts from 'typescript';
import {NgCompilerOptions} from '../../../../core/api';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* Ensures the left side of a nullish coalescing operation is nullable.
Expand Down Expand Up @@ -58,7 +63,10 @@ class NullishCoalescingNotNullableCheck extends TemplateCheckWithVisitor<ErrorCo
}
const diagnostic = ctx.makeTemplateDiagnostic(
templateMapping.span,
`The left side of this nullish coalescing operation does not include 'null' or 'undefined' in its type, therefore the '??' operator can be safely removed.`,
formatExtendedError(
ErrorCode.NULLISH_COALESCING_NOT_NULLABLE,
`The left side of this nullish coalescing operation does not include 'null' or 'undefined' in its type, therefore the '??' operator can be safely removed.`,
),
);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import ts from 'typescript';
import {NgCompilerOptions} from '../../../../core/api';
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

/**
* Ensures the left side of an optional chain operation is nullable.
Expand Down Expand Up @@ -85,7 +90,10 @@ class OptionalChainNotNullableCheck extends TemplateCheckWithVisitor<ErrorCode.O
: `the '?.' operator can be safely removed`;
const diagnostic = ctx.makeTemplateDiagnostic(
templateMapping.span,
`The left side of this optional chain operation does not include 'null' or 'undefined' in its type, therefore ${advice}.`,
formatExtendedError(
ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE,
`The left side of this optional chain operation does not include 'null' or 'undefined' in its type, therefore ${advice}`,
),
);
return [diagnostic];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
import {
TemplateCheckFactory,
TemplateCheckWithVisitor,
TemplateContext,
formatExtendedError,
} from '../../api';

const NG_SKIP_HYDRATION_ATTR_NAME = 'ngSkipHydration';

Expand All @@ -29,7 +34,11 @@ class NgSkipHydrationSpec extends TemplateCheckWithVisitor<ErrorCode.SKIP_HYDRAT
): NgTemplateDiagnostic<ErrorCode.SKIP_HYDRATION_NOT_STATIC>[] {
/** Binding should always error */
if (node instanceof TmplAstBoundAttribute && node.name === NG_SKIP_HYDRATION_ATTR_NAME) {
const errorString = `ngSkipHydration should not be used as a binding.`;
const errorString = formatExtendedError(
ErrorCode.SKIP_HYDRATION_NOT_STATIC,
`ngSkipHydration should not be used as a binding`,
);

const diagnostic = ctx.makeTemplateDiagnostic(node.sourceSpan, errorString);
return [diagnostic];
}
Expand All @@ -42,7 +51,11 @@ class NgSkipHydrationSpec extends TemplateCheckWithVisitor<ErrorCode.SKIP_HYDRAT
!acceptedValues.includes(node.value) &&
node.value !== undefined
) {
const errorString = `ngSkipHydration only accepts "true" or "" as value or no value at all. For example 'ngSkipHydration="true"' or 'ngSkipHydration'`;
const errorString = formatExtendedError(
ErrorCode.SKIP_HYDRATION_NOT_STATIC,
`ngSkipHydration only accepts "true" or "" as value or no value at all. For example 'ngSkipHydration="true"' or 'ngSkipHydration'`,
);

const diagnostic = ctx.makeTemplateDiagnostic(node.sourceSpan, errorString);
return [diagnostic];
}
Expand Down
Loading