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
refactor(compiler-cli): move the expression expression type checker #16562
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
325 changes: 325 additions & 0 deletions
325
packages/compiler-cli/src/diagnostics/expression_diagnostics.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,325 @@ | ||
/** | ||
* @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 {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveSummary, CompileTypeMetadata, DirectiveAst, ElementAst, EmbeddedTemplateAst, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, VariableAst, findNode, identifierName, templateVisitAll, tokenReference} from '@angular/compiler'; | ||
|
||
import {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './expression_type'; | ||
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols'; | ||
|
||
export interface DiagnosticTemplateInfo { | ||
fileName?: string; | ||
offset: number; | ||
query: SymbolQuery; | ||
members: SymbolTable; | ||
htmlAst: Node[]; | ||
templateAst: TemplateAst[]; | ||
} | ||
|
||
export interface ExpressionDiagnostic { | ||
message: string; | ||
span: Span; | ||
kind: DiagnosticKind; | ||
} | ||
|
||
export function getTemplateExpressionDiagnostics(info: DiagnosticTemplateInfo): | ||
ExpressionDiagnostic[] { | ||
const visitor = new ExpressionDiagnosticsVisitor( | ||
info, (path: TemplateAstPath, includeEvent: boolean) => | ||
getExpressionScope(info, path, includeEvent)); | ||
templateVisitAll(visitor, info.templateAst); | ||
return visitor.diagnostics; | ||
} | ||
|
||
export function getExpressionDiagnostics( | ||
scope: SymbolTable, ast: AST, query: SymbolQuery, | ||
context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] { | ||
const analyzer = new AstType(scope, query, context); | ||
analyzer.getDiagnostics(ast); | ||
return analyzer.diagnostics; | ||
} | ||
|
||
function getReferences(info: DiagnosticTemplateInfo): SymbolDeclaration[] { | ||
const result: SymbolDeclaration[] = []; | ||
|
||
function processReferences(references: ReferenceAst[]) { | ||
for (const reference of references) { | ||
let type: Symbol|undefined = undefined; | ||
if (reference.value) { | ||
type = info.query.getTypeSymbol(tokenReference(reference.value)); | ||
} | ||
result.push({ | ||
name: reference.name, | ||
kind: 'reference', | ||
type: type || info.query.getBuiltinType(BuiltinType.Any), | ||
get definition() { return getDefintionOf(info, reference); } | ||
}); | ||
} | ||
} | ||
|
||
const visitor = new class extends RecursiveTemplateAstVisitor { | ||
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { | ||
super.visitEmbeddedTemplate(ast, context); | ||
processReferences(ast.references); | ||
} | ||
visitElement(ast: ElementAst, context: any): any { | ||
super.visitElement(ast, context); | ||
processReferences(ast.references); | ||
} | ||
}; | ||
|
||
templateVisitAll(visitor, info.templateAst); | ||
|
||
return result; | ||
} | ||
|
||
function getDefintionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Definition|undefined { | ||
if (info.fileName) { | ||
const templateOffset = info.offset; | ||
return [{ | ||
fileName: info.fileName, | ||
span: { | ||
start: ast.sourceSpan.start.offset + templateOffset, | ||
end: ast.sourceSpan.end.offset + templateOffset | ||
} | ||
}]; | ||
} | ||
} | ||
|
||
function getVarDeclarations( | ||
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] { | ||
const result: SymbolDeclaration[] = []; | ||
|
||
let current = path.tail; | ||
while (current) { | ||
if (current instanceof EmbeddedTemplateAst) { | ||
for (const variable of current.variables) { | ||
const name = variable.name; | ||
|
||
// Find the first directive with a context. | ||
const context = | ||
current.directives.map(d => info.query.getTemplateContext(d.directive.type.reference)) | ||
.find(c => !!c); | ||
|
||
// Determine the type of the context field referenced by variable.value. | ||
let type: Symbol|undefined = undefined; | ||
if (context) { | ||
const value = context.get(variable.value); | ||
if (value) { | ||
type = value.type !; | ||
let kind = info.query.getTypeKind(type); | ||
if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) { | ||
// The any type is not very useful here. For special cases, such as ngFor, we can do | ||
// better. | ||
type = refinedVariableType(type, info, current); | ||
} | ||
} | ||
} | ||
if (!type) { | ||
type = info.query.getBuiltinType(BuiltinType.Any); | ||
} | ||
result.push({ | ||
name, | ||
kind: 'variable', type, get definition() { return getDefintionOf(info, variable); } | ||
}); | ||
} | ||
} | ||
current = path.parentOf(current); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function refinedVariableType( | ||
type: Symbol, info: DiagnosticTemplateInfo, templateElement: EmbeddedTemplateAst): Symbol { | ||
// Special case the ngFor directive | ||
const ngForDirective = templateElement.directives.find(d => { | ||
const name = identifierName(d.directive.type); | ||
return name == 'NgFor' || name == 'NgForOf'; | ||
}); | ||
if (ngForDirective) { | ||
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf'); | ||
if (ngForOfBinding) { | ||
const bindingType = new AstType(info.members, info.query, {}).getType(ngForOfBinding.value); | ||
if (bindingType) { | ||
const result = info.query.getElementType(bindingType); | ||
if (result) { | ||
return result; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// We can't do better, just return the original type. | ||
return type; | ||
} | ||
|
||
function getEventDeclaration(info: DiagnosticTemplateInfo, includeEvent?: boolean) { | ||
let result: SymbolDeclaration[] = []; | ||
if (includeEvent) { | ||
// TODO: Determine the type of the event parameter based on the Observable<T> or EventEmitter<T> | ||
// of the event. | ||
result = [{name: '$event', kind: 'variable', type: info.query.getBuiltinType(BuiltinType.Any)}]; | ||
} | ||
return result; | ||
} | ||
|
||
export function getExpressionScope( | ||
info: DiagnosticTemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable { | ||
let result = info.members; | ||
const references = getReferences(info); | ||
const variables = getVarDeclarations(info, path); | ||
const events = getEventDeclaration(info, includeEvent); | ||
if (references.length || variables.length || events.length) { | ||
const referenceTable = info.query.createSymbolTable(references); | ||
const variableTable = info.query.createSymbolTable(variables); | ||
const eventsTable = info.query.createSymbolTable(events); | ||
result = info.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]); | ||
} | ||
return result; | ||
} | ||
|
||
class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor { | ||
private path: TemplateAstPath; | ||
private directiveSummary: CompileDirectiveSummary; | ||
|
||
diagnostics: ExpressionDiagnostic[] = []; | ||
|
||
constructor( | ||
private info: DiagnosticTemplateInfo, | ||
private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) { | ||
super(); | ||
this.path = new AstPath<TemplateAst>([]); | ||
} | ||
|
||
visitDirective(ast: DirectiveAst, context: any): any { | ||
// Override the default child visitor to ignore the host properties of a directive. | ||
if (ast.inputs && ast.inputs.length) { | ||
templateVisitAll(this, ast.inputs, context); | ||
} | ||
} | ||
|
||
visitBoundText(ast: BoundTextAst): void { | ||
this.push(ast); | ||
this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false); | ||
this.pop(); | ||
} | ||
|
||
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void { | ||
this.push(ast); | ||
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); | ||
this.pop(); | ||
} | ||
|
||
visitElementProperty(ast: BoundElementPropertyAst): void { | ||
this.push(ast); | ||
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false); | ||
this.pop(); | ||
} | ||
|
||
visitEvent(ast: BoundEventAst): void { | ||
this.push(ast); | ||
this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true); | ||
this.pop(); | ||
} | ||
|
||
visitVariable(ast: VariableAst): void { | ||
const directive = this.directiveSummary; | ||
if (directive && ast.value) { | ||
const context = this.info.query.getTemplateContext(directive.type.reference) !; | ||
if (context && !context.has(ast.value)) { | ||
if (ast.value === '$implicit') { | ||
this.reportError( | ||
'The template context does not have an implicit value', spanOf(ast.sourceSpan)); | ||
} else { | ||
this.reportError( | ||
`The template context does not defined a member called '${ast.value}'`, | ||
spanOf(ast.sourceSpan)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
visitElement(ast: ElementAst, context: any): void { | ||
this.push(ast); | ||
super.visitElement(ast, context); | ||
this.pop(); | ||
} | ||
|
||
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { | ||
const previousDirectiveSummary = this.directiveSummary; | ||
|
||
this.push(ast); | ||
|
||
// Find directive that refernces this template | ||
this.directiveSummary = | ||
ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type)) !; | ||
|
||
// Process children | ||
super.visitEmbeddedTemplate(ast, context); | ||
|
||
this.pop(); | ||
|
||
this.directiveSummary = previousDirectiveSummary; | ||
} | ||
|
||
private attributeValueLocation(ast: TemplateAst) { | ||
const path = findNode(this.info.htmlAst, ast.sourceSpan.start.offset); | ||
const last = path.tail; | ||
if (last instanceof Attribute && last.valueSpan) { | ||
// Add 1 for the quote. | ||
return last.valueSpan.start.offset + 1; | ||
} | ||
return ast.sourceSpan.start.offset; | ||
} | ||
|
||
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) { | ||
const scope = this.getExpressionScope(this.path, includeEvent); | ||
this.diagnostics.push(...getExpressionDiagnostics(scope, ast, this.info.query, { | ||
event: includeEvent | ||
}).map(d => ({ | ||
span: offsetSpan(d.ast.span, offset + this.info.offset), | ||
kind: d.kind, | ||
message: d.message | ||
}))); | ||
} | ||
|
||
private push(ast: TemplateAst) { this.path.push(ast); } | ||
|
||
private pop() { this.path.pop(); } | ||
|
||
private reportError(message: string, span: Span|undefined) { | ||
if (span) { | ||
this.diagnostics.push( | ||
{span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Error, message}); | ||
} | ||
} | ||
|
||
private reportWarning(message: string, span: Span) { | ||
this.diagnostics.push( | ||
{span: offsetSpan(span, this.info.offset), kind: DiagnosticKind.Warning, message}); | ||
} | ||
} | ||
|
||
function hasTemplateReference(type: CompileTypeMetadata): boolean { | ||
if (type.diDeps) { | ||
for (let diDep of type.diDeps) { | ||
if (diDep.token && diDep.token.identifier && | ||
identifierName(diDep.token !.identifier !) == 'TemplateRef') | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function offsetSpan(span: Span, amount: number): Span { | ||
return {start: span.start + amount, end: span.end + amount}; | ||
} | ||
|
||
function spanOf(sourceSpan: ParseSourceSpan): Span { | ||
return {start: sourceSpan.start.offset, end: sourceSpan.end.offset}; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Ah, never mind.