diff --git a/packages/compiler-cli/src/diagnostics/expression_type.ts b/packages/compiler-cli/src/diagnostics/expression_type.ts index 72be38975e..cdecaa0f7d 100644 --- a/packages/compiler-cli/src/diagnostics/expression_type.ts +++ b/packages/compiler-cli/src/diagnostics/expression_type.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, visitAstChildren} from '@angular/compiler'; +import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, visitAstChildren} from '@angular/compiler'; import {BuiltinType, Signature, Span, Symbol, SymbolQuery, SymbolTable} from './symbols'; @@ -283,6 +283,11 @@ export class AstType implements AstVisitor { return this.query.getBuiltinType(BuiltinType.Boolean); } + visitNonNullAssert(ast: NonNullAssert) { + const expressionType = this.getType(ast.expression); + return this.query.getNonNullableType(expressionType); + } + visitPropertyRead(ast: PropertyRead) { return this.resolvePropertyRead(this.getType(ast.receiver), ast); } diff --git a/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts index 518e117cfa..db2ceceacd 100644 --- a/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts +++ b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts @@ -135,6 +135,8 @@ describe('expression diagnostics', () => { () => reject(`
{{maybe_person.name.first}}`, 'The expression might be null')); it('should accept a safe accss to an undefined field', () => accept(`
{{maybe_person?.name.first}}
`)); + it('should accept a type assert to an undefined field', + () => accept(`
{{maybe_person!.name.first}}
`)); it('should accept a # reference', () => accept(`
diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 2847598afd..2924cd10a4 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -348,6 +348,11 @@ class _AstToIrVisitor implements cdAst.AstVisitor { return convertToStatementIfNeeded(mode, o.not(this._visit(ast.expression, _Mode.Expression))); } + visitNonNullAssert(ast: cdAst.NonNullAssert, mode: _Mode): any { + return convertToStatementIfNeeded( + mode, o.assertNotNull(this._visit(ast.expression, _Mode.Expression))); + } + visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any { const leftMostSafe = this.leftMostSafeNode(ast); if (leftMostSafe) { @@ -509,6 +514,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { visitMethodCall(ast: cdAst.MethodCall) { return visit(this, ast.receiver); }, visitPipe(ast: cdAst.BindingPipe) { return null; }, visitPrefixNot(ast: cdAst.PrefixNot) { return null; }, + visitNonNullAssert(ast: cdAst.NonNullAssert) { return null; }, visitPropertyRead(ast: cdAst.PropertyRead) { return visit(this, ast.receiver); }, visitPropertyWrite(ast: cdAst.PropertyWrite) { return null; }, visitQuote(ast: cdAst.Quote) { return null; }, @@ -547,6 +553,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { visitMethodCall(ast: cdAst.MethodCall) { return true; }, visitPipe(ast: cdAst.BindingPipe) { return true; }, visitPrefixNot(ast: cdAst.PrefixNot) { return visit(this, ast.expression); }, + visitNonNullAssert(ast: cdAst.PrefixNot) { return visit(this, ast.expression); }, visitPropertyRead(ast: cdAst.PropertyRead) { return false; }, visitPropertyWrite(ast: cdAst.PropertyWrite) { return false; }, visitQuote(ast: cdAst.Quote) { return false; }, diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 2db3d28504..3f3417c8b5 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -166,6 +166,13 @@ export class PrefixNot extends AST { } } +export class NonNullAssert extends AST { + constructor(span: ParseSpan, public expression: AST) { super(span); } + visit(visitor: AstVisitor, context: any = null): any { + return visitor.visitNonNullAssert(this, context); + } +} + export class MethodCall extends AST { constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) { super(span); @@ -222,6 +229,7 @@ export interface AstVisitor { visitMethodCall(ast: MethodCall, context: any): any; visitPipe(ast: BindingPipe, context: any): any; visitPrefixNot(ast: PrefixNot, context: any): any; + visitNonNullAssert(ast: NonNullAssert, context: any): any; visitPropertyRead(ast: PropertyRead, context: any): any; visitPropertyWrite(ast: PropertyWrite, context: any): any; visitQuote(ast: Quote, context: any): any; @@ -230,7 +238,7 @@ export interface AstVisitor { visit?(ast: AST, context?: any): any; } -export class NullAstVisitor { +export class NullAstVisitor implements AstVisitor { visitBinary(ast: Binary, context: any): any {} visitChain(ast: Chain, context: any): any {} visitConditional(ast: Conditional, context: any): any {} @@ -245,6 +253,7 @@ export class NullAstVisitor { visitMethodCall(ast: MethodCall, context: any): any {} visitPipe(ast: BindingPipe, context: any): any {} visitPrefixNot(ast: PrefixNot, context: any): any {} + visitNonNullAssert(ast: NonNullAssert, context: any): any {} visitPropertyRead(ast: PropertyRead, context: any): any {} visitPropertyWrite(ast: PropertyWrite, context: any): any {} visitQuote(ast: Quote, context: any): any {} @@ -303,6 +312,10 @@ export class RecursiveAstVisitor implements AstVisitor { ast.expression.visit(this); return null; } + visitNonNullAssert(ast: NonNullAssert, context: any): any { + ast.expression.visit(this); + return null; + } visitPropertyRead(ast: PropertyRead, context: any): any { ast.receiver.visit(this); return null; @@ -379,6 +392,10 @@ export class AstTransformer implements AstVisitor { return new PrefixNot(ast.span, ast.expression.visit(this)); } + visitNonNullAssert(ast: NonNullAssert, context: any): AST { + return new NonNullAssert(ast.span, ast.expression.visit(this)); + } + visitConditional(ast: Conditional, context: any): AST { return new Conditional( ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this)); @@ -461,6 +478,7 @@ export function visitAstChildren(ast: AST, visitor: AstVisitor, context?: any) { visitAll(ast.args); }, visitPrefixNot(ast) { visit(ast.expression); }, + visitNonNullAssert(ast) { visit(ast.expression); }, visitPropertyRead(ast) { visit(ast.receiver); }, visitPropertyWrite(ast) { visit(ast.receiver); diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 73b202cbe2..958120a11f 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -10,7 +10,8 @@ import * as chars from '../chars'; import {CompilerInjectable} from '../injectable'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config'; import {escapeRegExp} from '../util'; -import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast'; + +import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast'; import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer'; @@ -523,6 +524,9 @@ export class _ParseAST { this.expectCharacter(chars.$RPAREN); result = new FunctionCall(this.span(result.span.start), result, args); + } else if (this.optionalOperator('!')) { + result = new NonNullAssert(this.span(result.span.start), result); + } else { return result; } @@ -811,6 +815,8 @@ class SimpleExpressionChecker implements AstVisitor { visitPrefixNot(ast: PrefixNot, context: any) {} + visitNonNullAssert(ast: NonNullAssert, context: any) {} + visitConditional(ast: Conditional, context: any) {} visitPipe(ast: BindingPipe, context: any) { this.errors.push('pipes'); } diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index 0a0d0784b7..f6ce372f72 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -344,6 +344,10 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex ast.condition.visitExpression(this, ctx); return null; } + visitAssertNotNullExpr(ast: o.AssertNotNull, ctx: EmitterVisitorContext): any { + ast.condition.visitExpression(this, ctx); + return null; + } abstract visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any; abstract visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: any): any; diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 84f0aedd6f..286a1931d9 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -377,6 +377,15 @@ export class NotExpr extends Expression { } } +export class AssertNotNull extends Expression { + constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) { + super(condition.type, sourceSpan); + } + visitExpression(visitor: ExpressionVisitor, context: any): any { + return visitor.visitAssertNotNullExpr(this, context); + } +} + export class CastExpr extends Expression { constructor(public value: Expression, type?: Type|null, sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); @@ -503,6 +512,7 @@ export interface ExpressionVisitor { visitExternalExpr(ast: ExternalExpr, context: any): any; visitConditionalExpr(ast: ConditionalExpr, context: any): any; visitNotExpr(ast: NotExpr, context: any): any; + visitAssertNotNullExpr(ast: AssertNotNull, context: any): any; visitCastExpr(ast: CastExpr, context: any): any; visitFunctionExpr(ast: FunctionExpr, context: any): any; visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any; @@ -768,6 +778,11 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { new NotExpr(ast.condition.visitExpression(this, context), ast.sourceSpan), context); } + visitAssertNotNullExpr(ast: AssertNotNull, context: any): any { + return this.transformExpr( + new AssertNotNull(ast.condition.visitExpression(this, context), ast.sourceSpan), context); + } + visitCastExpr(ast: CastExpr, context: any): any { return this.transformExpr( new CastExpr(ast.value.visitExpression(this, context), ast.type, ast.sourceSpan), context); @@ -948,6 +963,10 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor ast.condition.visitExpression(this, context); return ast; } + visitAssertNotNullExpr(ast: AssertNotNull, context: any): any { + ast.condition.visitExpression(this, context); + return ast; + } visitCastExpr(ast: CastExpr, context: any): any { ast.value.visitExpression(this, context); return ast; @@ -1139,6 +1158,11 @@ export function not(expr: Expression, sourceSpan?: ParseSourceSpan | null): NotE return new NotExpr(expr, sourceSpan); } +export function assertNotNull( + expr: Expression, sourceSpan?: ParseSourceSpan | null): AssertNotNull { + return new AssertNotNull(expr, sourceSpan); +} + export function fn( params: FnParam[], body: Statement[], type?: Type | null, sourceSpan?: ParseSourceSpan | null): FunctionExpr { diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts index 17b0b90d3d..2a58d9b5a8 100644 --- a/packages/compiler/src/output/output_interpreter.ts +++ b/packages/compiler/src/output/output_interpreter.ts @@ -233,6 +233,9 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { visitNotExpr(ast: o.NotExpr, ctx: _ExecutionContext): any { return !ast.condition.visitExpression(this, ctx); } + visitAssertNotNullExpr(ast: o.AssertNotNull, ctx: _ExecutionContext): any { + return ast.condition.visitExpression(this, ctx); + } visitCastExpr(ast: o.CastExpr, ctx: _ExecutionContext): any { return ast.value.visitExpression(this, ctx); } diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index a813aab8fe..636fc9d383 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -129,6 +129,12 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor return null; } + visitAssertNotNullExpr(ast: o.AssertNotNull, ctx: EmitterVisitorContext): any { + const result = super.visitAssertNotNullExpr(ast, ctx); + ctx.print(ast, '!'); + return result; + } + visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any { if (ctx.isExportedVar(stmt.name) && stmt.value instanceof o.ExternalExpr && !stmt.type) { // check for a reexport diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index 81752f5ac5..fa675a92b0 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -14,7 +14,7 @@ import * as ts from 'typescript'; import {extractSourceMap, originalPositionFor} from '../output/source_map_util'; -import {EmittingCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, compile, settings, setup, toMockFileArray} from './test_util'; +import {EmittingCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, compile, expectNoDiagnostics, settings, setup, toMockFileArray} from './test_util'; describe('compiler (unbundled Angular)', () => { let angularFiles = setup(); @@ -215,6 +215,32 @@ describe('compiler (unbundled Angular)', () => { }); })); + + it('should be able to supress a null access', async(() => { + const FILES: MockDirectory = { + app: { + 'app.ts': ` + import {Component, NgModule} from '@angular/core'; + + interface Person { name: string; } + + @Component({ + selector: 'my-comp', + template: '{{maybe_person!.name}}' + }) + export class MyComp { + maybe_person?: Person; + } + + @NgModule({ + declarations: [MyComp] + }) + export class MyModule {} + ` + } + }; + compile([FILES, angularFiles], {postCompile: expectNoDiagnostics}); + })); }); it('should add the preamble to generated files', async(() => { diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index c76ad271c3..4b7f75fc1f 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -44,6 +44,7 @@ export const settings: ts.CompilerOptions = { removeComments: false, noImplicitAny: false, skipLibCheck: true, + strictNullChecks: true, lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'], types: [] }; diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index b1199dab63..a71fdd99c2 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -112,6 +112,12 @@ export function main() { checkAction('!!!true'); }); + it('should parse postfix ! expression', () => { + checkAction('true!'); + checkAction('a!.b'); + checkAction('a!!!!.b'); + }); + it('should parse multiplicative expressions', () => { checkAction('3*4/2%5', '3 * 4 / 2 % 5'); }); diff --git a/packages/compiler/test/expression_parser/unparser.ts b/packages/compiler/test/expression_parser/unparser.ts index 93c793b85c..308852f732 100644 --- a/packages/compiler/test/expression_parser/unparser.ts +++ b/packages/compiler/test/expression_parser/unparser.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast'; +import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; class Unparser implements AstVisitor { @@ -156,6 +156,11 @@ class Unparser implements AstVisitor { this._visit(ast.expression); } + visitNonNullAssert(ast: NonNullAssert, context: any) { + this._visit(ast.expression); + this._expression += '!'; + } + visitSafePropertyRead(ast: SafePropertyRead, context: any) { this._visit(ast.receiver); this._expression += `?.${ast.name}`; diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts index d783c598bc..fa6f65cbba 100644 --- a/packages/compiler/test/output/js_emitter_spec.ts +++ b/packages/compiler/test/output/js_emitter_spec.ts @@ -146,6 +146,7 @@ export function main() { const lhs = o.variable('lhs'); const rhs = o.variable('rhs'); expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;'); + expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar;'); expect( emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt())) .toEqual('(someVar? trueCase: falseCase);'); diff --git a/packages/compiler/test/output/ts_emitter_spec.ts b/packages/compiler/test/output/ts_emitter_spec.ts index f3f08cd2e0..1aec3dcbb9 100644 --- a/packages/compiler/test/output/ts_emitter_spec.ts +++ b/packages/compiler/test/output/ts_emitter_spec.ts @@ -232,6 +232,7 @@ export function main() { const rhs = o.variable('rhs'); expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('(someVar);'); expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;'); + expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar!;'); expect( emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt())) .toEqual('(someVar? trueCase: falseCase);'); diff --git a/packages/language-service/src/expressions.ts b/packages/language-service/src/expressions.ts index 2f3b24e8df..9af51ec457 100644 --- a/packages/language-service/src/expressions.ts +++ b/packages/language-service/src/expressions.ts @@ -69,6 +69,7 @@ export function getExpressionCompletions( } }, visitPrefixNot(ast) {}, + visitNonNullAssert(ast) {}, visitPropertyRead(ast) { const receiverType = getType(ast.receiver); result = receiverType ? receiverType.members() : scope; @@ -138,6 +139,7 @@ export function getExpressionSymbol( } }, visitPrefixNot(ast) {}, + visitNonNullAssert(ast) {}, visitPropertyRead(ast) { const receiverType = getType(ast.receiver); symbol = receiverType && receiverType.members().get(ast.name);