feat(compiler): support a non-null postfix assert (#16672)
Template expressions can now use a post-fix `!` operator that asserts the target of the operator is not null. This is similar to the TypeScript non-null assert operator. Expressions generated in factories will be generated with the non-null assert operator. Closes: #10855
This commit is contained in:
parent
2eca6e67e1
commit
b9521b568f
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -135,6 +135,8 @@ describe('expression diagnostics', () => {
|
|||
() => reject(`<div>{{maybe_person.name.first}}`, 'The expression might be null'));
|
||||
it('should accept a safe accss to an undefined field',
|
||||
() => accept(`<div>{{maybe_person?.name.first}}</div>`));
|
||||
it('should accept a type assert to an undefined field',
|
||||
() => accept(`<div>{{maybe_person!.name.first}}</div>`));
|
||||
it('should accept a # reference', () => accept(`
|
||||
<form #f="ngForm" novalidate>
|
||||
<input name="first" ngModel required #first="ngModel">
|
||||
|
|
|
@ -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; },
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'); }
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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: []
|
||||
};
|
||||
|
|
|
@ -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'); });
|
||||
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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);');
|
||||
|
|
|
@ -232,6 +232,7 @@ export function main() {
|
|||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('(<number>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);');
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue