refactor(compiler-cli): add infrastructure for new diagnostics (#60977)

We need a couple of custom diagnostics for selectorless. These changes add the infrastructure so they can be reported.

PR Close #60977
This commit is contained in:
Kristiyan Kostadinov 2025-04-23 11:59:15 +02:00 committed by Miles Malerba
parent bc9a067ef4
commit e89ebf3dc7
4 changed files with 123 additions and 2 deletions

View file

@ -58,6 +58,7 @@ export enum ErrorCode {
IMPORT_CYCLE_DETECTED = 3003,
IMPORT_GENERATION_FAILURE = 3004,
INACCESSIBLE_DEFERRED_TRIGGER_ELEMENT = 8010,
INCORRECT_NAMED_TEMPLATE_DEPENDENCY_TYPE = 2025,
INCORRECTLY_DECLARED_ON_STATIC_MEMBER = 1100,
INITIALIZER_API_DECORATOR_METADATA_COLLISION = 1051,
INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY = 1053,
@ -73,6 +74,7 @@ export enum ErrorCode {
LOCAL_COMPILATION_UNRESOLVED_CONST = 11001,
LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION = 11003,
MISSING_CONTROL_FLOW_DIRECTIVE = 8103,
MISSING_NAMED_TEMPLATE_DEPENDENCY = 2024,
MISSING_NGFOROF_LET = 8105,
MISSING_PIPE = 8004,
MISSING_REFERENCE_TARGET = 8003,
@ -104,6 +106,7 @@ export enum ErrorCode {
SYMBOL_NOT_EXPORTED = 3001,
TEMPLATE_PARSE_ERROR = 5002,
TEXT_ATTRIBUTE_NOT_BINDING = 8104,
UNCLAIMED_DIRECTIVE_BINDING = 8018,
UNDECORATED_CLASS_USING_ANGULAR_FEATURES = 2007,
UNDECORATED_PROVIDER = 2005,
UNINVOKED_FUNCTION_IN_EVENT_BINDING = 8111,

View file

@ -162,6 +162,17 @@ export enum ErrorCode {
*/
NON_STANDALONE_NOT_ALLOWED = 2023,
/**
* Raised when a named template dependency isn't defined in the component's source file.
*/
MISSING_NAMED_TEMPLATE_DEPENDENCY = 2024,
/**
* Raised if an incorrect type is used for a named template dependency (e.g. directive
* class used as a component).
*/
INCORRECT_NAMED_TEMPLATE_DEPENDENCY_TYPE = 2025,
SYMBOL_NOT_EXPORTED = 3001,
/**
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be
@ -374,6 +385,12 @@ export enum ErrorCode {
/** A `@let` declaration conflicts with another symbol in the same scope. */
CONFLICTING_LET_DECLARATION = 8017,
/**
* A binding inside selectorless directive syntax did
* not match any inputs/outputs of the directive.
*/
UNCLAIMED_DIRECTIVE_BINDING = 8018,
/**
* A two way binding in a template has an incorrect syntax,
* parentheses outside brackets. For example:

View file

@ -13,6 +13,8 @@ import {
PropertyWrite,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstForLoopBlock,
TmplAstForLoopBlockEmpty,
@ -23,6 +25,7 @@ import {
TmplAstReference,
TmplAstSwitchBlockCase,
TmplAstTemplate,
TmplAstTextAttribute,
TmplAstVariable,
TmplAstViewportDeferredTrigger,
} from '@angular/compiler';
@ -125,7 +128,7 @@ export interface OutOfBandDiagnosticRecorder {
/** Reports required inputs that haven't been bound. */
missingRequiredInputs(
id: TypeCheckId,
element: TmplAstElement | TmplAstTemplate,
element: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
directiveName: string,
isComponent: boolean,
inputAliases: string[],
@ -185,6 +188,34 @@ export interface OutOfBandDiagnosticRecorder {
* @param current the `TmplAstLetDeclaration` which is invalid.
*/
conflictingDeclaration(id: TypeCheckId, current: TmplAstLetDeclaration): void;
/**
* Reports that a named template dependency (e.g. `<Missing/>`) is not available.
* @param id Type checking ID of the template in which the dependency is declared.
* @param node Node that declares the dependency.
*/
missingNamedTemplateDependency(id: TypeCheckId, node: TmplAstComponent | TmplAstDirective): void;
/**
* Reports that a templace dependency of the wrong kind has been referenced at a specific position
* (e.g. `<SomeDirective/>`).
* @param id Type checking ID of the template in which the dependency is declared.
* @param node Node that declares the dependency.
*/
incorrectTemplateDependencyType(id: TypeCheckId, node: TmplAstComponent | TmplAstDirective): void;
/**
* Reports a binding inside directive syntax that does not match any of the inputs/outputs of
* the directive.
* @param id Type checking ID of the template in which the directive was defined.
* @param directive Directive that contains the binding.
* @param node Node declaring the binding.
*/
unclaimedDirectiveBinding(
id: TypeCheckId,
directive: TmplAstDirective,
node: TmplAstBoundAttribute | TmplAstTextAttribute | TmplAstBoundEvent,
): void;
}
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
@ -467,7 +498,7 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
missingRequiredInputs(
id: TypeCheckId,
element: TmplAstElement | TmplAstTemplate,
element: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
directiveName: string,
isComponent: boolean,
inputAliases: string[],
@ -656,6 +687,58 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
),
);
}
missingNamedTemplateDependency(id: TypeCheckId, node: TmplAstComponent | TmplAstDirective): void {
this._diagnostics.push(
makeTemplateDiagnostic(
id,
this.resolver.getTemplateSourceMapping(id),
node.startSourceSpan,
ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.MISSING_NAMED_TEMPLATE_DEPENDENCY),
// Wording is meant to mimic the wording TS uses in their diagnostic for missing symbols.
`Cannot find name "${node instanceof TmplAstDirective ? node.name : node.componentName}".`,
),
);
}
incorrectTemplateDependencyType(
id: TypeCheckId,
node: TmplAstComponent | TmplAstDirective,
): void {
this._diagnostics.push(
makeTemplateDiagnostic(
id,
this.resolver.getTemplateSourceMapping(id),
node.startSourceSpan,
ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.INCORRECT_NAMED_TEMPLATE_DEPENDENCY_TYPE),
`Incorrect reference type. Type must be an ${node instanceof TmplAstComponent ? '@Component' : '@Directive'}.`,
),
);
}
unclaimedDirectiveBinding(
id: TypeCheckId,
directive: TmplAstDirective,
node: TmplAstBoundAttribute | TmplAstTextAttribute | TmplAstBoundEvent,
): void {
const errorMsg =
`Directive ${directive.name} does not have an ` +
`${node instanceof TmplAstBoundEvent ? 'output' : 'input'} named "${node.name}". ` +
`Bindings to directives must target existing inputs or outputs.`;
this._diagnostics.push(
makeTemplateDiagnostic(
id,
this.resolver.getTemplateSourceMapping(id),
node.keySpan || node.sourceSpan,
ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.UNCLAIMED_DIRECTIVE_BINDING),
errorMsg,
),
);
}
}
function makeInlineDiagnostic(

View file

@ -17,8 +17,13 @@ import {
R3TargetBinder,
SelectorlessMatcher,
SelectorMatcher,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
TmplAstLetDeclaration,
TmplAstTextAttribute,
} from '@angular/compiler';
import {readFileSync} from 'fs';
import path from 'path';
@ -1023,4 +1028,17 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
target: TmplAstLetDeclaration,
): void {}
conflictingDeclaration(id: TypeCheckId, current: TmplAstLetDeclaration): void {}
missingNamedTemplateDependency(
id: TypeCheckId,
node: TmplAstComponent | TmplAstDirective,
): void {}
unclaimedDirectiveBinding(
id: TypeCheckId,
directive: TmplAstDirective,
node: TmplAstBoundAttribute | TmplAstTextAttribute | TmplAstBoundEvent,
): void {}
incorrectTemplateDependencyType(
id: TypeCheckId,
node: TmplAstComponent | TmplAstDirective,
): void {}
}