Skip to content

Commit

Permalink
fix(compiler-cli): more accurate diagnostics for host binding parser …
Browse files Browse the repository at this point in the history
…errors (#58870)

Currently host bindings are in a bit of a weird state, because their source spans all point to the root object literal, rather than the individual expression. This is tricky to handle at the moment, because the object is being passed around as a `Record<string, string>` since the compiler needs to support both JIT and non-JIT environments, and because the AOT compiler evaluates the entire literal rather than doing it expression-by-expression. As a result, when we report errors in one of the host bindings, we end up highlighting the entire expression which can be very noisy in an IDE.

These changes aim to report a more accurate error for the most common case where the `host` object is initialized to a `string -> string` object literal by matching the failing expression to one of the property initializers. Note that this isn't 100% reliable, because we can't map cases like `host: SOME_CONST`, but it's still better than the current setup.

PR Close #58870
  • Loading branch information
crisbeto authored and thePunderWoman committed Nov 25, 2024
1 parent ea0bf74 commit fb1fa8b
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ParsedHostBindings,
ParseError,
parseHostBindings,
ParserError,
R3DirectiveMetadata,
R3HostDirectiveMetadata,
R3InputMetadata,
Expand Down Expand Up @@ -1629,17 +1630,40 @@ function evaluateHostExpressionBindings(
const errors = verifyHostBindings(bindings, createSourceSpan(hostExpr));
if (errors.length > 0) {
throw new FatalDiagnosticError(
// TODO: provide more granular diagnostic and output specific host expression that
// triggered an error instead of the whole host object.
ErrorCode.HOST_BINDING_PARSE_ERROR,
hostExpr,
getHostBindingErrorNode(errors[0], hostExpr),
errors.map((error: ParseError) => error.msg).join('\n'),
);
}

return bindings;
}

/**
* Attempts to match a parser error to the host binding expression that caused it.
* @param error Error to match.
* @param hostExpr Expression declaring the host bindings.
*/
function getHostBindingErrorNode(error: ParseError, hostExpr: ts.Expression): ts.Node {
// In the most common case the `host` object is an object literal with string values. We can
// confidently match the error to its expression by looking at the string value that the parser
// failed to parse and the initializers for each of the properties. If we fail to match, we fall
// back to the old behavior where the error is reported on the entire `host` object.
if (ts.isObjectLiteralExpression(hostExpr) && error.relatedError instanceof ParserError) {
for (const prop of hostExpr.properties) {
if (
ts.isPropertyAssignment(prop) &&
ts.isStringLiteralLike(prop.initializer) &&
prop.initializer.text === error.relatedError.input
) {
return prop.initializer;
}
}
}

return hostExpr;
}

/**
* Extracts and prepares the host directives metadata from an array literal expression.
* @param rawHostDirectives Expression that defined the `hostDirectives`.
Expand Down
10 changes: 5 additions & 5 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5114,9 +5114,7 @@ runInEachFileSystem((os: string) => {
);

const errors = env.driveDiagnostics();
expect(getDiagnosticSourceCode(errors[0])).toBe(`{
'(click)': 'act() | pipe',
}`);
expect(getDiagnosticSourceCode(errors[0])).toBe(`'act() | pipe'`);
expect(errors[0].messageText).toContain('/test.ts@7:17');
});

Expand Down Expand Up @@ -5158,10 +5156,12 @@ runInEachFileSystem((os: string) => {
class FooCmp {}
`,
);
const errors = env.driveDiagnostics();
expect(trim(errors[0].messageText as string)).toContain(
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(trim(diags[0].messageText as string)).toContain(
'Host binding expression cannot contain pipes',
);
expect(getDiagnosticSourceCode(diags[0])).toBe(`'id | myPipe'`);
});

it('should generate host bindings for directives', () => {
Expand Down
14 changes: 11 additions & 3 deletions packages/compiler/src/parse_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,17 @@ export enum ParseErrorLevel {

export class ParseError {
constructor(
public span: ParseSourceSpan,
public msg: string,
public level: ParseErrorLevel = ParseErrorLevel.ERROR,
/** Location of the error. */
readonly span: ParseSourceSpan,
/** Error message. */
readonly msg: string,
/** Severity level of the error. */
readonly level: ParseErrorLevel = ParseErrorLevel.ERROR,
/**
* Error that caused the error to be surfaced. For example, an error in a sub-expression that
* couldn't be parsed. Not guaranteed to be defined, but can be used to provide more context.
*/
readonly relatedError?: unknown,
) {}

contextualMessage(): string {
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/src/template_parser/binding_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,13 +770,14 @@ export class BindingParser {
message: string,
sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.ERROR,
relatedError?: ParserError,
) {
this.errors.push(new ParseError(sourceSpan, message, level));
this.errors.push(new ParseError(sourceSpan, message, level, relatedError));
}

private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
for (const error of errors) {
this._reportError(error.message, sourceSpan);
this._reportError(error.message, sourceSpan, undefined, error);
}
}

Expand Down

0 comments on commit fb1fa8b

Please sign in to comment.