feat(compiler): support number separators in templates (#42672)
As of ES2021, JavaScript allows using underscores as separators inside numbers, in order to make them more readable (e.g. `1_000_000` vs `1000000`). TypeScript has had support for separators for a while so these changes expand the template parser to handle them as well. PR Close #42672
This commit is contained in:
parent
234b5edcc7
commit
9f5cc7c808
@ -631,3 +631,53 @@ export declare class MyModule {
|
|||||||
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
|
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****************************************************************************************************
|
||||||
|
* PARTIAL FILE: number_separator.js
|
||||||
|
****************************************************************************************************/
|
||||||
|
import { Component, NgModule } from '@angular/core';
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export class MyApp {
|
||||||
|
constructor() {
|
||||||
|
this.multiplier = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "my-app", ngImport: i0, template: `
|
||||||
|
<div>Total: \${{ 1_000_000 * multiplier }}</div>
|
||||||
|
<span>Remaining: \${{ 123_456.78_9 / 2 }}</span>
|
||||||
|
`, isInline: true });
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
selector: 'my-app',
|
||||||
|
template: `
|
||||||
|
<div>Total: \${{ 1_000_000 * multiplier }}</div>
|
||||||
|
<span>Remaining: \${{ 123_456.78_9 / 2 }}</span>
|
||||||
|
`
|
||||||
|
}]
|
||||||
|
}] });
|
||||||
|
export class MyModule {
|
||||||
|
}
|
||||||
|
MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
||||||
|
MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] });
|
||||||
|
MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule });
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{
|
||||||
|
type: NgModule,
|
||||||
|
args: [{ declarations: [MyApp] }]
|
||||||
|
}] });
|
||||||
|
|
||||||
|
/****************************************************************************************************
|
||||||
|
* PARTIAL FILE: number_separator.d.ts
|
||||||
|
****************************************************************************************************/
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export declare class MyApp {
|
||||||
|
multiplier: number;
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
|
||||||
|
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never>;
|
||||||
|
}
|
||||||
|
export declare class MyModule {
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
|
||||||
|
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyApp], never, never>;
|
||||||
|
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -272,6 +272,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "should support number literals with separators",
|
||||||
|
"inputFiles": [
|
||||||
|
"number_separator.ts"
|
||||||
|
],
|
||||||
|
"expectations": [
|
||||||
|
{
|
||||||
|
"failureMessage": "Invalid number literal",
|
||||||
|
"files": [
|
||||||
|
"number_separator.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
template: function MyApp_Template(rf, ctx) {
|
||||||
|
// ...
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵadvance(1);
|
||||||
|
$r3$.ɵɵtextInterpolate1("Total: $", 1000000 * ctx.multiplier, "");
|
||||||
|
$r3$.ɵɵadvance(2);
|
||||||
|
$r3$.ɵɵtextInterpolate1("Remaining: $", 123456.789 / 2, "");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: `
|
||||||
|
<div>Total: \${{ 1_000_000 * multiplier }}</div>
|
||||||
|
<span>Remaining: \${{ 123_456.78_9 / 2 }}</span>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class MyApp {
|
||||||
|
multiplier = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyApp]})
|
||||||
|
export class MyModule {
|
||||||
|
}
|
@ -303,12 +303,24 @@ class _Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scanNumber(start: number): Token {
|
scanNumber(start: number): Token {
|
||||||
let simple: boolean = (this.index === start);
|
let simple = (this.index === start);
|
||||||
|
let hasSeparators = false;
|
||||||
this.advance(); // Skip initial digit.
|
this.advance(); // Skip initial digit.
|
||||||
while (true) {
|
while (true) {
|
||||||
if (chars.isDigit(this.peek)) {
|
if (chars.isDigit(this.peek)) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if (this.peek == chars.$PERIOD) {
|
} else if (this.peek === chars.$_) {
|
||||||
|
// Separators are only valid when they're surrounded by digits. E.g. `1_0_1` is
|
||||||
|
// valid while `_101` and `101_` are not. The separator can't be next to the decimal
|
||||||
|
// point or another separator either. Note that it's unlikely that we'll hit a case where
|
||||||
|
// the underscore is at the start, because that's a valid identifier and it will be picked
|
||||||
|
// up earlier in the parsing. We validate for it anyway just in case.
|
||||||
|
if (!chars.isDigit(this.input.charCodeAt(this.index - 1)) ||
|
||||||
|
!chars.isDigit(this.input.charCodeAt(this.index + 1))) {
|
||||||
|
return this.error('Invalid numeric separator', 0);
|
||||||
|
}
|
||||||
|
hasSeparators = true;
|
||||||
|
} else if (this.peek === chars.$PERIOD) {
|
||||||
simple = false;
|
simple = false;
|
||||||
} else if (isExponentStart(this.peek)) {
|
} else if (isExponentStart(this.peek)) {
|
||||||
this.advance();
|
this.advance();
|
||||||
@ -320,8 +332,12 @@ class _Scanner {
|
|||||||
}
|
}
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
const str: string = this.input.substring(start, this.index);
|
|
||||||
const value: number = simple ? parseIntAutoRadix(str) : parseFloat(str);
|
let str = this.input.substring(start, this.index);
|
||||||
|
if (hasSeparators) {
|
||||||
|
str = str.replace(/_/g, '');
|
||||||
|
}
|
||||||
|
const value = simple ? parseIntAutoRadix(str) : parseFloat(str);
|
||||||
return newNumberToken(start, this.index, value);
|
return newNumberToken(start, this.index, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +295,36 @@ function expectErrorToken(token: Token, index: any, end: number, message: string
|
|||||||
it('should tokenize ?? as operator', () => {
|
it('should tokenize ?? as operator', () => {
|
||||||
expectOperatorToken(lex('??')[0], 0, 2, '??');
|
expectOperatorToken(lex('??')[0], 0, 2, '??');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should tokenize number with separator', () => {
|
||||||
|
expectNumberToken(lex('123_456')[0], 0, 7, 123_456);
|
||||||
|
expectNumberToken(lex('1_000_000_000')[0], 0, 13, 1_000_000_000);
|
||||||
|
expectNumberToken(lex('123_456.78')[0], 0, 10, 123_456.78);
|
||||||
|
expectNumberToken(lex('123_456_789.123_456_789')[0], 0, 23, 123_456_789.123_456_789);
|
||||||
|
expectNumberToken(lex('1_2_3_4')[0], 0, 7, 1_2_3_4);
|
||||||
|
expectNumberToken(lex('1_2_3_4.5_6_7_8')[0], 0, 15, 1_2_3_4.5_6_7_8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should tokenize number starting with an underscore as an identifier', () => {
|
||||||
|
expectIdentifierToken(lex('_123')[0], 0, 4, '_123');
|
||||||
|
expectIdentifierToken(lex('_123_')[0], 0, 5, '_123_');
|
||||||
|
expectIdentifierToken(lex('_1_2_3_')[0], 0, 7, '_1_2_3_');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid number separators', () => {
|
||||||
|
expectErrorToken(
|
||||||
|
lex('123_')[0], 3, 3,
|
||||||
|
'Lexer Error: Invalid numeric separator at column 3 in expression [123_]');
|
||||||
|
expectErrorToken(
|
||||||
|
lex('12__3')[0], 2, 2,
|
||||||
|
'Lexer Error: Invalid numeric separator at column 2 in expression [12__3]');
|
||||||
|
expectErrorToken(
|
||||||
|
lex('1_2_3_.456')[0], 5, 5,
|
||||||
|
'Lexer Error: Invalid numeric separator at column 5 in expression [1_2_3_.456]');
|
||||||
|
expectErrorToken(
|
||||||
|
lex('1_2_3._456')[0], 6, 6,
|
||||||
|
'Lexer Error: Invalid numeric separator at column 6 in expression [1_2_3._456]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2061,6 +2061,19 @@ describe('acceptance integration tests', () => {
|
|||||||
expect(fixture.componentInstance.directive.value).toEqual({a: 1, b: 2, someProp: 3});
|
expect(fixture.componentInstance.directive.value).toEqual({a: 1, b: 2, someProp: 3});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle numeric separators in templates', () => {
|
||||||
|
@Component({template: 'Balance: ${{ 1_000_000 * multiplier }}'})
|
||||||
|
class App {
|
||||||
|
multiplier = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('Balance: $5000000');
|
||||||
|
});
|
||||||
|
|
||||||
describe('tView.firstUpdatePass', () => {
|
describe('tView.firstUpdatePass', () => {
|
||||||
function isFirstUpdatePass() {
|
function isFirstUpdatePass() {
|
||||||
const lView = getLView();
|
const lView = getLView();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user