New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unknown pipe & local ref diagnostics #33454
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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.io/license | ||
*/ | ||
|
||
import {AbsoluteSourceSpan, BindingPipe, TmplAstReference} from '@angular/compiler'; | ||
import * as ts from 'typescript'; | ||
|
||
import {ErrorCode, ngErrorCode} from '../../diagnostics'; | ||
|
||
import {TcbSourceResolver, absoluteSourceSpanToSourceLocation, makeTemplateDiagnostic} from './diagnostics'; | ||
|
||
|
||
|
||
/** | ||
* Collects `ts.Diagnostic`s on problems which occur in the template which aren't directly sourced | ||
* from Type Check Blocks. | ||
* | ||
* During the creation of a Type Check Block, the template is traversed and the | ||
* `OutOfBandDiagnosticRecorder` is called to record cases when a correct interpretation for the | ||
* template cannot be found. These operations create `ts.Diagnostic`s which are stored by the | ||
* recorder for later display. | ||
*/ | ||
export interface OutOfBandDiagnosticRecorder { | ||
readonly diagnostics: ReadonlyArray<ts.Diagnostic>; | ||
|
||
/** | ||
* Reports a `#ref="target"` expression in the template for which a target directive could not be | ||
* found. | ||
* | ||
* @param templateId the template type-checking ID of the template which contains the broken | ||
* reference. | ||
* @param ref the `TmplAstReference` which could not be matched to a directive. | ||
*/ | ||
missingReferenceTarget(templateId: string, ref: TmplAstReference): void; | ||
|
||
/** | ||
* Reports usage of a `| pipe` expression in the template for which the named pipe could not be | ||
* found. | ||
* | ||
* @param templateId the template type-checking ID of the template which contains the unknown | ||
* pipe. | ||
* @param ast the `BindingPipe` invocation of the pipe which could not be found. | ||
* @param sourceSpan the `AbsoluteSourceSpan` of the pipe invocation (ideally, this should be the | ||
* source span of the pipe's name). This depends on the source span of the `BindingPipe` itself | ||
* plus span of the larger expression context. | ||
*/ | ||
missingPipe(templateId: string, ast: BindingPipe, sourceSpan: AbsoluteSourceSpan): void; | ||
} | ||
|
||
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { | ||
private _diagnostics: ts.Diagnostic[] = []; | ||
|
||
constructor(private resolver: TcbSourceResolver) {} | ||
|
||
get diagnostics(): ReadonlyArray<ts.Diagnostic> { return this._diagnostics; } | ||
|
||
missingReferenceTarget(templateId: string, ref: TmplAstReference): void { | ||
const mapping = this.resolver.getSourceMapping(templateId); | ||
const value = ref.value.trim(); | ||
|
||
const errorMsg = `No directive found with exportAs '${value}'.`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For future consideration: have an intuitive way of visualising the module scope/available directive exports to better indicate what needs to happen to resolve this diagnostic. Because it's a non-local issue, how to fix it is not straightforward (and one of the areas I would like to see Angular become better is in error reporting!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I completely agree. One of the goals of having the centralized |
||
this._diagnostics.push(makeTemplateDiagnostic( | ||
mapping, ref.valueSpan || ref.sourceSpan, ts.DiagnosticCategory.Error, | ||
ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), errorMsg)); | ||
} | ||
|
||
missingPipe(templateId: string, ast: BindingPipe, absSpan: AbsoluteSourceSpan): void { | ||
const mapping = this.resolver.getSourceMapping(templateId); | ||
const errorMsg = `No pipe found with name '${ast.name}'.`; | ||
|
||
const location = absoluteSourceSpanToSourceLocation(templateId, absSpan); | ||
const sourceSpan = this.resolver.sourceLocationToSpan(location); | ||
if (sourceSpan === null) { | ||
throw new Error( | ||
`Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`); | ||
} | ||
this._diagnostics.push(makeTemplateDiagnostic( | ||
mapping, sourceSpan, ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.MISSING_PIPE), | ||
errorMsg)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import {addParseSpanInfo, addSourceId, toAbsoluteSpan, wrapForDiagnostics} from | |
import {DomSchemaChecker} from './dom'; | ||
import {Environment} from './environment'; | ||
import {NULL_AS_ANY, astToTypescript} from './expression'; | ||
import {OutOfBandDiagnosticRecorder} from './oob'; | ||
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util'; | ||
|
||
|
||
|
@@ -28,15 +29,27 @@ import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsC | |
* When passed through TypeScript's TypeChecker, type errors that arise within the type check block | ||
* function indicate issues in the template itself. | ||
* | ||
* @param node the TypeScript node for the component class. | ||
* As a side effect of generating a TCB for the component, `ts.Diagnostic`s may also be produced | ||
* directly for issues within the template which are identified during generation. These issues are | ||
* recorded in either the `domSchemaChecker` (which checks usage of DOM elements and bindings) as | ||
* well as the `oobRecorder` (which records errors when the type-checking code generator is unable | ||
* to sufficiently understand a template). | ||
* | ||
* @param env an `Environment` into which type-checking code will be generated. | ||
* @param ref a `Reference` to the component class which should be type-checked. | ||
* @param name a `ts.Identifier` to use for the generated `ts.FunctionDeclaration`. | ||
* @param meta metadata about the component's template and the function being generated. | ||
* @param importManager an `ImportManager` for the file into which the TCB will be written. | ||
* @param domSchemaChecker used to check and record errors regarding improper usage of DOM elements | ||
* and bindings. | ||
* @param oobRecorder used to record errors regarding template elements which could not be correctly | ||
* translated into types during TCB generation. | ||
*/ | ||
export function generateTypeCheckBlock( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should make clear that this function has a side-effect when it comes to producing diagnostics. Having the out of bands parameter does indicate this somewhat, but it could be made more explicit. Come to think of it, it would be a good idea for future reference to document There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I updated and expanded the comment to talk about the verifications which happen during generation. As mentioned above, I think the "right way" to do this is to refactor the template traversal to be independent of everything, and hang various operations off of this central traversal, including both the verification diagnostics and TCB generation. |
||
env: Environment, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, name: ts.Identifier, | ||
meta: TypeCheckBlockMetadata, domSchemaChecker: DomSchemaChecker): ts.FunctionDeclaration { | ||
const tcb = | ||
new Context(env, domSchemaChecker, meta.id, meta.boundTarget, meta.pipes, meta.schemas); | ||
meta: TypeCheckBlockMetadata, domSchemaChecker: DomSchemaChecker, | ||
oobRecorder: OutOfBandDiagnosticRecorder): ts.FunctionDeclaration { | ||
const tcb = new Context( | ||
env, domSchemaChecker, oobRecorder, meta.id, meta.boundTarget, meta.pipes, meta.schemas); | ||
const scope = Scope.forNodes(tcb, null, tcb.boundTarget.target.template !); | ||
const ctxRawType = env.referenceType(ref); | ||
if (!ts.isTypeReferenceNode(ctxRawType)) { | ||
|
@@ -562,7 +575,8 @@ export class Context { | |
private nextId = 1; | ||
|
||
constructor( | ||
readonly env: Environment, readonly domSchemaChecker: DomSchemaChecker, readonly id: string, | ||
readonly env: Environment, readonly domSchemaChecker: DomSchemaChecker, | ||
readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: string, | ||
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>, | ||
private pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>, | ||
readonly schemas: SchemaMetadata[]) {} | ||
|
@@ -575,9 +589,9 @@ export class Context { | |
*/ | ||
allocateId(): ts.Identifier { return ts.createIdentifier(`_t${this.nextId++}`); } | ||
|
||
getPipeByName(name: string): ts.Expression { | ||
getPipeByName(name: string): ts.Expression|null { | ||
if (!this.pipes.has(name)) { | ||
throw new Error(`Missing pipe: ${name}`); | ||
return null; | ||
} | ||
return this.env.pipeInst(this.pipes.get(name) !); | ||
} | ||
|
@@ -797,6 +811,7 @@ class Scope { | |
for (const child of node.children) { | ||
this.appendNode(child); | ||
} | ||
this.checkReferencesOfNode(node); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am surprised you took this approach, intertwining the reporting of missing refs with the generation of TCBs, instead of adding some diagnostic reporting facility to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered this, actually. Part of the problem is that the way we map expressions back into the original template source depends a lot on the way we traverse the template. This doesn't matter for Additionally, it's easier within the TCB generator to conform to the Ultimately we might refactor in this direction. Especially because the HTML-level template parsing errors are not currently surfaced via our "nice" diagnostics mechanism. We'll eventually want to port that over too. Maybe it would make more sense to refactor the template traversal out of the TCB code, and make TCB generation a side effect as well. So the core template typechecking engine would walk through the template, and call into each "plugin" to do things like check reference validity, check the DOM schema, and cause TCBs to be emitted into the type check file. |
||
} else if (node instanceof TmplAstTemplate) { | ||
// Template children are rendered in a child scope. | ||
this.appendDirectivesAndInputsOfNode(node); | ||
|
@@ -806,11 +821,20 @@ class Scope { | |
this.templateCtxOpMap.set(node, ctxIndex); | ||
this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node)); | ||
} | ||
this.checkReferencesOfNode(node); | ||
} else if (node instanceof TmplAstBoundText) { | ||
this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node)); | ||
} | ||
} | ||
|
||
private checkReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void { | ||
for (const ref of node.references) { | ||
if (this.tcb.boundTarget.getReferenceTarget(ref) === null) { | ||
this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref); | ||
} | ||
} | ||
} | ||
|
||
private appendDirectivesAndInputsOfNode(node: TmplAstElement|TmplAstTemplate): void { | ||
// Collect all the inputs on the element. | ||
const claimedInputs = new Set<string>(); | ||
|
@@ -964,9 +988,17 @@ class TcbExpressionTranslator { | |
return ts.createIdentifier('ctx'); | ||
} else if (ast instanceof BindingPipe) { | ||
const expr = this.translate(ast.exp); | ||
let pipe: ts.Expression; | ||
let pipe: ts.Expression|null; | ||
if (this.tcb.env.config.checkTypeOfPipes) { | ||
pipe = this.tcb.getPipeByName(ast.name); | ||
if (pipe === null) { | ||
// No pipe by that name exists in scope. Record this as an error. | ||
const nameAbsoluteSpan = toAbsoluteSpan(ast.nameSpan, this.sourceSpan); | ||
this.tcb.oobRecorder.missingPipe(this.tcb.id, ast, nameAbsoluteSpan); | ||
|
||
// Return an 'any' value to at least allow the rest of the expression to be checked. | ||
pipe = NULL_AS_ANY; | ||
} | ||
} else { | ||
pipe = ts.createParen(ts.createAsExpression( | ||
ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); | ||
|
@@ -1025,7 +1057,10 @@ class TcbExpressionTranslator { | |
} else if (binding instanceof TmplAstReference) { | ||
const target = this.tcb.boundTarget.getReferenceTarget(binding); | ||
if (target === null) { | ||
throw new Error(`Unbound reference? ${binding.name}`); | ||
// This reference is unbound. Traversal of the `TmplAstReference` itself should have | ||
// recorded the error in the `OutOfBandDiagnosticRecorder`. | ||
// Still check the rest of the expression if possible by using an `any` value. | ||
return NULL_AS_ANY; | ||
} | ||
|
||
// The reference is either to an element, an <ng-template> node, or to a directive on an | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, maybe we should just decompose
TcbSourceResolver.sourceLocationToSpan
to receive two arguments instead of theSourceLocation
bundle.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm all for refactoring this to clean it up :)