test(compiler): allow asserting matching identifier names (#22835)
PR Close #22835
This commit is contained in:
parent
129bb1800b
commit
5ab9d4d437
|
@ -25,7 +25,7 @@ import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToM
|
||||||
|
|
||||||
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
|
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
|
||||||
const OPERATOR =
|
const OPERATOR =
|
||||||
/!|%|\*|\/|\^|&{1,2}|\|{1,2}|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!={1,2}|=>|\+{1,2}|-{1,2}|@|,|\.|\.\.\./;
|
/!|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./;
|
||||||
const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/;
|
const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/;
|
||||||
const NUMBER = /\d+/;
|
const NUMBER = /\d+/;
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ const TOKEN = new RegExp(
|
||||||
type Piece = string | RegExp;
|
type Piece = string | RegExp;
|
||||||
|
|
||||||
const SKIP = /(?:.|\n|\r)*/;
|
const SKIP = /(?:.|\n|\r)*/;
|
||||||
const MATCHING_IDENT = /^\$.*\$$/;
|
|
||||||
|
|
||||||
|
const ERROR_CONTEXT_WIDTH = 30;
|
||||||
// Transform the expected output to set of tokens
|
// Transform the expected output to set of tokens
|
||||||
function tokenize(text: string): Piece[] {
|
function tokenize(text: string): Piece[] {
|
||||||
TOKEN.lastIndex = 0;
|
TOKEN.lastIndex = 0;
|
||||||
|
@ -57,23 +57,26 @@ function tokenize(text: string): Piece[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TOKEN.lastIndex !== 0) {
|
if (pieces.length === 0 || TOKEN.lastIndex !== 0) {
|
||||||
const from = TOKEN.lastIndex;
|
const from = TOKEN.lastIndex;
|
||||||
const to = from + 30;
|
const to = from + ERROR_CONTEXT_WIDTH;
|
||||||
throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`)
|
throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pieces;
|
return pieces;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectEmit(source: string, expected: string, description: string) {
|
export function expectEmit(
|
||||||
|
source: string, expected: string, description: string,
|
||||||
|
assertIdentifiers?: {[name: string]: RegExp}) {
|
||||||
const pieces = tokenize(expected);
|
const pieces = tokenize(expected);
|
||||||
const expr = r(pieces);
|
const {regexp, groups} = buildMatcher(pieces);
|
||||||
if (!expr.test(source)) {
|
const matches = source.match(regexp);
|
||||||
|
if (matches === null) {
|
||||||
let last: number = 0;
|
let last: number = 0;
|
||||||
for (let i = 1; i < pieces.length; i++) {
|
for (let i = 1; i < pieces.length; i++) {
|
||||||
const t = r(pieces.slice(0, i));
|
const {regexp} = buildMatcher(pieces.slice(0, i));
|
||||||
const m = source.match(t);
|
const m = source.match(regexp);
|
||||||
const expectedPiece = pieces[i - 1] == IDENTIFIER ? '<IDENT>' : pieces[i - 1];
|
const expectedPiece = pieces[i - 1] == IDENTIFIER ? '<IDENT>' : pieces[i - 1];
|
||||||
if (!m) {
|
if (!m) {
|
||||||
fail(
|
fail(
|
||||||
|
@ -85,11 +88,42 @@ export function expectEmit(source: string, expected: string, description: string
|
||||||
}
|
}
|
||||||
fail(
|
fail(
|
||||||
`Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`);
|
`Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`);
|
||||||
|
} else {
|
||||||
|
if (assertIdentifiers) {
|
||||||
|
// It might be possible to add the constraints in the original regexp (see `buildMatcher`)
|
||||||
|
// by transforming the assertion regexps when using anchoring, grouping, back references,
|
||||||
|
// flags, ...
|
||||||
|
//
|
||||||
|
// Checking identifiers after they have matched allows for a simple and flexible
|
||||||
|
// implementation.
|
||||||
|
// The overall performance are not impacted when `assertIdentifiers` is empty.
|
||||||
|
const ids = Object.keys(assertIdentifiers);
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
const id = ids[i];
|
||||||
|
if (groups.has(id)) {
|
||||||
|
const name = matches[groups.get(id) as number];
|
||||||
|
const regexp = assertIdentifiers[id];
|
||||||
|
if (!regexp.test(name)) {
|
||||||
|
throw Error(
|
||||||
|
`${description}: The matching identifier "${id}" is "${name}" which doesn't match ${regexp}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IDENT_LIKE = /^[a-z][A-Z]/;
|
const IDENT_LIKE = /^[a-z][A-Z]/;
|
||||||
function r(pieces: (string | RegExp)[]): RegExp {
|
const MATCHING_IDENT = /^\$.*\$$/;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Builds a regexp that matches the given `pieces`
|
||||||
|
*
|
||||||
|
* It returns:
|
||||||
|
* - the `regexp` to be used to match the generated code,
|
||||||
|
* - the `groups` which maps `$...$` identifier to their position in the regexp matches.
|
||||||
|
*/
|
||||||
|
function buildMatcher(pieces: (string | RegExp)[]): {regexp: RegExp, groups: Map<string, number>} {
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
let group = 0;
|
let group = 0;
|
||||||
|
@ -116,7 +150,10 @@ function r(pieces: (string | RegExp)[]): RegExp {
|
||||||
results.push('(?:' + piece.source + ')');
|
results.push('(?:' + piece.source + ')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new RegExp(results.join(''));
|
return {
|
||||||
|
regexp: new RegExp(results.join('')),
|
||||||
|
groups,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function doCompile(
|
function doCompile(
|
||||||
|
@ -164,8 +201,6 @@ function doCompile(
|
||||||
new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
|
new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
|
||||||
elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector);
|
elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create the TypeScript program
|
// Create the TypeScript program
|
||||||
const sourceFiles = program.getSourceFiles().map(sf => sf.fileName);
|
const sourceFiles = program.getSourceFiles().map(sf => sf.fileName);
|
||||||
|
|
||||||
|
|
|
@ -151,4 +151,38 @@ describe('mock_compiler', () => {
|
||||||
result.source, '$ctx$.$name$ … $ctx$.$name$.length',
|
result.source, '$ctx$.$name$ … $ctx$.$name$.length',
|
||||||
'could not find correct length access');
|
'could not find correct length access');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to enforce that identifiers match a regexp', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'hello.component.ts': `
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'})
|
||||||
|
export class HelloComponent {
|
||||||
|
@Input() name: string = 'world';
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'hello.module.ts': `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {HelloComponent} from './hello.component';
|
||||||
|
|
||||||
|
@NgModule({declarations: [HelloComponent]})
|
||||||
|
export class HelloModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
|
||||||
|
// Pass: `$n$` ends with `ME` in the generated code
|
||||||
|
expectEmit(result.source, '$ctx$.$n$ … $ctx$.$n$.length', 'Match names', {'$n$': /ME$/i});
|
||||||
|
|
||||||
|
// Fail: `$n$` does not match `/(not)_(\1)/` in the generated code
|
||||||
|
expect(() => {
|
||||||
|
expectEmit(
|
||||||
|
result.source, '$ctx$.$n$ … $ctx$.$n$.length', 'Match names', {'$n$': /(not)_(\1)/});
|
||||||
|
}).toThrowError(/"\$n\$" is "name" which doesn't match \/\(not\)_\(\\1\)\//);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
Loading…
Reference in New Issue