Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support more binary assignment operators in templates #62064

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 31 additions & 25 deletions adev/src/content/guide/templates/expression-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,37 @@ For example, `@for` blocks make several local variables corresponding to informa

Angular supports the following operators from standard JavaScript.

| Operator | Example(s) |
| --------------------- | ---------------------------------------- |
| Add / Concatenate | `1 + 2` |
| Subtract | `52 - 3` |
| Multiply | `41 * 6` |
| Divide | `20 / 4` |
| Remainder (Modulo) | `17 % 5` |
| Exponentiation | `10 ** 3` |
| Parenthesis | `9 * (8 + 4)` |
| Conditional (Ternary) | `a > b ? true : false` |
| And (Logical) | `&&` |
| Or (Logical) | `\|\|` |
| Not (Logical) | `!` |
| Nullish Coalescing | `possiblyNullValue ?? 'default'` |
| Comparison Operators | `<`, `<=`, `>`, `>=`, `==`, `===`, `!==` |
| Unary Negation | `-x` |
| Unary Plus | `+y` |
| Property Accessor | `person['name']` |
| typeof | `typeof 42` |
| void | `void 1` |
| in | `'model' in car` |
| Operator | Example(s) |
| ------------------------------- | ---------------------------------------- |
| Add / Concatenate | `1 + 2` |
| Subtract | `52 - 3` |
| Multiply | `41 * 6` |
| Divide | `20 / 4` |
| Remainder (Modulo) | `17 % 5` |
| Exponentiation | `10 ** 3` |
| Parenthesis | `9 * (8 + 4)` |
| Conditional (Ternary) | `a > b ? true : false` |
| And (Logical) | `&&` |
| Or (Logical) | `\|\|` |
| Not (Logical) | `!` |
| Nullish Coalescing | `possiblyNullValue ?? 'default'` |
| Comparison Operators | `<`, `<=`, `>`, `>=`, `==`, `===`, `!==` |
| Unary Negation | `-x` |
| Unary Plus | `+y` |
| Property Accessor | `person['name']` |
| typeof | `typeof 42` |
| void | `void 1` |
| in | `'model' in car` |
| Assignment | `a = b` |
| Addition Assignment | `a += b` |
| Subtraction Assignment | `a -= b` |
| Multiplication Assignment | `a *= b` |
| Division Assignment | `a /= b` |
| Remainder Assignment | `a %= b` |
| Exponentiation Assignment | `a **= b` |
| Logical AND Assignment | `a &&= b` |
| Logical OR Assignment | `a \|\|= b` |
| Nullish Coalescing Assignment | `a ??= b` |

Angular expressions additionally also support the following non-standard operators:

Expand All @@ -84,13 +94,9 @@ NOTE: Optional chaining behaves differently from the standard JavaScript version
| Operator | Example(s) |
| --------------------- | --------------------------------- |
| All bitwise operators | `&`, `&=`, `~`, `\|=`, `^=`, etc. |
| Assignment operators | `=` |
| Object destructuring | `const { name } = person` |
| Array destructuring | `const [firstItem] = items` |
| Comma operator | `x = (x++, x)` |
| in | `'model' in car` |
| typeof | `typeof 42` |
| void | `void 1` |
| instanceof | `car instanceof Automobile` |
| new | `new Car()` |

Expand Down
19 changes: 17 additions & 2 deletions packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {

createArrayLiteral = t.arrayExpression;

createAssignment(target: t.Expression, value: t.Expression): t.Expression {
createAssignment(
target: t.Expression,
operator: BinaryOperator,
value: t.Expression,
): t.Expression {
assert(target, isLExpression, 'must be a left hand side expression');
return t.assignmentExpression('=', target, value);
return t.assignmentExpression(operator, target, value);
}

createBinaryExpression(
Expand All @@ -52,6 +56,17 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
case '||':
case '??':
return t.logicalExpression(operator, leftOperand, rightOperand);
case '=':
case '+=':
case '-=':
case '*=':
case '/=':
case '%=':
case '**=':
case '&&=':
case '||=':
case '??=':
throw new Error(`Unexpected assignment operator ${operator}`);
default:
return t.binaryExpression(operator, leftOperand, rightOperand);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('BabelAstFactory', () => {
it('should create an assignment node using the target and value expressions', () => {
const target = expression.ast`x`;
const value = expression.ast`42`;
const assignment = factory.createAssignment(target, value);
const assignment = factory.createAssignment(target, '=', value);
expect(generate(assignment).code).toEqual('x = 42');
});
});
Expand Down
15 changes: 13 additions & 2 deletions packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ export interface AstFactory<TStatement, TExpression> {
* Create an assignment expression (e.g. `lhsExpr = rhsExpr`).
*
* @param target an expression that evaluates to the left side of the assignment.
* @param operator binary assignment operator that will be applied.
* @param value an expression that evaluates to the right side of the assignment.
*/
createAssignment(target: TExpression, value: TExpression): TExpression;
createAssignment(target: TExpression, operator: BinaryOperator, value: TExpression): TExpression;

/**
* Create a binary expression (e.g. `lhs && rhs`).
Expand Down Expand Up @@ -317,7 +318,17 @@ export type BinaryOperator =
| '||'
| '+'
| '??'
| 'in';
| 'in'
| '='
| '+='
| '-='
| '*='
| '/='
| '%='
| '**='
| '&&='
| '||='
| '??=';

/**
* The original location of the start or end of a node created by the `AstFactory`.
Expand Down
24 changes: 19 additions & 5 deletions packages/compiler-cli/src/ngtsc/translator/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ const BINARY_OPERATORS = /* @__PURE__ */ new Map<o.BinaryOperator, BinaryOperato
[o.BinaryOperator.NullishCoalesce, '??'],
[o.BinaryOperator.Exponentiation, '**'],
[o.BinaryOperator.In, 'in'],
[o.BinaryOperator.Assign, '='],
[o.BinaryOperator.AdditionAssignment, '+='],
[o.BinaryOperator.SubtractionAssignment, '-='],
[o.BinaryOperator.MultiplicationAssignment, '*='],
[o.BinaryOperator.DivisionAssignment, '/='],
[o.BinaryOperator.RemainderAssignment, '%='],
[o.BinaryOperator.ExponentiationAssignment, '**='],
[o.BinaryOperator.AndAssignment, '&&='],
[o.BinaryOperator.OrAssignment, '||='],
[o.BinaryOperator.NullishCoalesceAssignment, '??='],
]);

export type RecordWrappedNodeFn<TExpression> = (node: o.WrappedNodeExpr<TExpression>) => void;
Expand Down Expand Up @@ -331,19 +341,23 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
}

visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, context: Context): TExpression {
if (ast.operator === o.BinaryOperator.Assign) {
if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${o.BinaryOperator[ast.operator]}`);
}

const operator = BINARY_OPERATORS.get(ast.operator)!;

if (ast.isAssignment()) {
return this.factory.createAssignment(
ast.lhs.visitExpression(this, context),
operator,
ast.rhs.visitExpression(this, context),
);
}

if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${o.BinaryOperator[ast.operator]}`);
}
return this.factory.createBinaryExpression(
ast.lhs.visitExpression(this, context),
BINARY_OPERATORS.get(ast.operator)!,
operator,
ast.rhs.visitExpression(this, context),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ const BINARY_OPERATORS: Record<BinaryOperator, ts.BinaryOperator> = /* @__PURE__
'+': ts.SyntaxKind.PlusToken,
'??': ts.SyntaxKind.QuestionQuestionToken,
'in': ts.SyntaxKind.InKeyword,
'=': ts.SyntaxKind.EqualsToken,
'+=': ts.SyntaxKind.PlusEqualsToken,
'-=': ts.SyntaxKind.MinusEqualsToken,
'*=': ts.SyntaxKind.AsteriskEqualsToken,
'/=': ts.SyntaxKind.SlashEqualsToken,
'%=': ts.SyntaxKind.PercentEqualsToken,
'**=': ts.SyntaxKind.AsteriskAsteriskEqualsToken,
'&&=': ts.SyntaxKind.AmpersandAmpersandEqualsToken,
'||=': ts.SyntaxKind.BarBarEqualsToken,
'??=': ts.SyntaxKind.QuestionQuestionEqualsToken,
}))();

const VAR_TYPES: Record<VariableDeclarationType, ts.NodeFlags> = /* @__PURE__ */ (() => ({
Expand All @@ -81,8 +91,12 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express

createArrayLiteral = ts.factory.createArrayLiteralExpression;

createAssignment(target: ts.Expression, value: ts.Expression): ts.Expression {
return ts.factory.createBinaryExpression(target, ts.SyntaxKind.EqualsToken, value);
createAssignment(
target: ts.Expression,
operator: BinaryOperator,
value: ts.Expression,
): ts.Expression {
return ts.factory.createBinaryExpression(target, BINARY_OPERATORS[operator], value);
}

createBinaryExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('TypeScriptAstFactory', () => {
items: [target, value],
generate,
} = setupExpressions(`x`, `42`);
const assignment = factory.createAssignment(target, value);
const assignment = factory.createAssignment(target, '=', value);
expect(generate(assignment)).toEqual('x = 42');
});
});
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ const BINARY_OPS = new Map<string, ts.BinaryOperator>([
['|', ts.SyntaxKind.BarToken],
['??', ts.SyntaxKind.QuestionQuestionToken],
['in', ts.SyntaxKind.InKeyword],
['=', ts.SyntaxKind.EqualsToken],
['+=', ts.SyntaxKind.PlusEqualsToken],
['-=', ts.SyntaxKind.MinusEqualsToken],
['*=', ts.SyntaxKind.AsteriskEqualsToken],
['/=', ts.SyntaxKind.SlashEqualsToken],
['%=', ts.SyntaxKind.PercentEqualsToken],
['**=', ts.SyntaxKind.AsteriskAsteriskEqualsToken],
['&&=', ts.SyntaxKind.AmpersandAmpersandEqualsToken],
['||=', ts.SyntaxKind.BarBarEqualsToken],
['??=', ts.SyntaxKind.QuestionQuestionEqualsToken],
]);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ export class SymbolBuilder {
// Also skipping SafePropertyReads as it breaks nullish coalescing not nullable extended diagnostic
if (
expression instanceof Binary &&
expression.operation === '=' &&
Binary.isAssignmentOperation(expression.operation) &&
expression.left instanceof PropertyRead
) {
withSpan = expression.left.nameSpan;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3117,7 +3117,7 @@ class TcbExpressionTranslator {
return targetExpression;
} else if (
ast instanceof Binary &&
ast.operation === '=' &&
Binary.isAssignmentOperation(ast.operation) &&
ast.left instanceof PropertyRead &&
ast.left.receiver instanceof ImplicitReceiver
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class ExpressionsSemanticsVisitor extends RecursiveAstVisitor {
}

override visitBinary(ast: Binary, context: TmplAstNode): void {
if (ast.operation === '=' && ast.left instanceof PropertyRead) {
if (Binary.isAssignmentOperation(ast.operation) && ast.left instanceof PropertyRead) {
this.checkForIllegalWriteInEventBinding(ast.left, context);
} else {
super.visitBinary(ast, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ describe('type check blocks', () => {
expect(tcb('{{!(void a === "object")}}')).toContain('!(((void (((this).a))) === ("object")))');
});

it('should handle assignment expressions', () => {
expect(tcb('<b (click)="a = b"></b>')).toContain('(((this).a)) = (((this).b));');
expect(tcb('<b (click)="a += b"></b>')).toContain('(((this).a)) += (((this).b));');
expect(tcb('<b (click)="a -= b"></b>')).toContain('(((this).a)) -= (((this).b));');
expect(tcb('<b (click)="a *= b"></b>')).toContain('(((this).a)) *= (((this).b));');
expect(tcb('<b (click)="a /= b"></b>')).toContain('(((this).a)) /= (((this).b));');
expect(tcb('<b (click)="a %= b"></b>')).toContain('(((this).a)) %= (((this).b));');
expect(tcb('<b (click)="a **= b"></b>')).toContain('(((this).a)) **= (((this).b));');
expect(tcb('<b (click)="a &&= b"></b>')).toContain('(((this).a)) &&= (((this).b));');
expect(tcb('<b (click)="a ||= b"></b>')).toContain('(((this).a)) ||= (((this).b));');
expect(tcb('<b (click)="a ??= b"></b>')).toContain('(((this).a)) ??= (((this).b));');
});

it('should handle exponentiation expressions', () => {
expect(tcb('{{a * b ** c + d}}')).toContain(
'(((((this).a)) * ((((this).b)) ** (((this).c)))) + (((this).d)))',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,18 @@ export class IdentityPipe {
transform(value) {
return value;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, isStandalone: true, name: "identity" });
}
IdentityPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
IdentityPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, isStandalone: true, name: "identity" });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, decorators: [{
type: Pipe,
args: [{ name: 'identity' }]
}] });
export class MyApp {
constructor() {
this.foo = { bar: 'baz' };
}
}
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
foo = { bar: 'baz' };
number = 1;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
{{ 1 + 2 }}
{{ (1 % 2) + 3 / 4 * 5 ** 6 }}
{{ +1 }}
Expand All @@ -100,7 +98,17 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-
{{ void 'test' }}
{{ (-1) ** 3 }}
{{ 'bar' in foo }}
<button (click)="number += 1"></button>
<button (click)="number -= 1"></button>
<button (click)="number *= 1"></button>
<button (click)="number /= 1"></button>
<button (click)="number %= 1"></button>
<button (click)="number **= 1"></button>
<button (click)="number &&= 1"></button>
<button (click)="number ||= 1"></button>
<button (click)="number ??= 1"></button>
`, isInline: true, dependencies: [{ kind: "pipe", type: IdentityPipe, name: "identity" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
type: Component,
args: [{
Expand All @@ -115,6 +123,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
{{ void 'test' }}
{{ (-1) ** 3 }}
{{ 'bar' in foo }}
<button (click)="number += 1"></button>
<button (click)="number -= 1"></button>
<button (click)="number *= 1"></button>
<button (click)="number /= 1"></button>
<button (click)="number %= 1"></button>
<button (click)="number **= 1"></button>
<button (click)="number &&= 1"></button>
<button (click)="number ||= 1"></button>
<button (click)="number ??= 1"></button>
`,
imports: [IdentityPipe],
}]
Expand All @@ -133,6 +150,7 @@ export declare class MyApp {
foo: {
bar?: string;
};
number: number;
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
{
"description": "should handle binary and unary operators",
"compilerOptions": {
"target": "es2020"
"target": "es2022"
},
"inputFiles": ["operators.ts"],
"expectations": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ export class IdentityPipe {
{{ void 'test' }}
{{ (-1) ** 3 }}
{{ 'bar' in foo }}
<button (click)="number += 1"></button>
<button (click)="number -= 1"></button>
<button (click)="number *= 1"></button>
<button (click)="number /= 1"></button>
<button (click)="number %= 1"></button>
<button (click)="number **= 1"></button>
<button (click)="number &&= 1"></button>
<button (click)="number ||= 1"></button>
<button (click)="number ??= 1"></button>
`,
imports: [IdentityPipe],
})
export class MyApp {
foo: {bar?: string} = {bar: 'baz'};
number = 1;
}
Loading
Loading