test(compiler): allow asserting matching identifier names (#22835)

PR Close #22835
This commit is contained in:
Victor Berchet 2018-03-16 17:16:12 -07:00 committed by Matias Niemelä
parent 129bb1800b
commit 5ab9d4d437
2 changed files with 87 additions and 18 deletions

View File

@ -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);

View File

@ -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\)\//);
});
}); });