From 5ab9d4d437ab27fa8df248012af895204136ca8e Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 16 Mar 2018 17:16:12 -0700 Subject: [PATCH] test(compiler): allow asserting matching identifier names (#22835) PR Close #22835 --- .../compiler/test/render3/mock_compile.ts | 63 ++++++++++++++----- .../test/render3/mock_compiler_spec.ts | 42 +++++++++++-- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts index 68fbbcd6cb..1a4203a412 100644 --- a/packages/compiler/test/render3/mock_compile.ts +++ b/packages/compiler/test/render3/mock_compile.ts @@ -25,7 +25,7 @@ import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToM const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; const OPERATOR = - /!|%|\*|\/|\^|&{1,2}|\|{1,2}|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!={1,2}|=>|\+{1,2}|-{1,2}|@|,|\.|\.\.\./; + /!|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./; const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/; const NUMBER = /\d+/; @@ -37,8 +37,8 @@ const TOKEN = new RegExp( type Piece = string | RegExp; const SKIP = /(?:.|\n|\r)*/; -const MATCHING_IDENT = /^\$.*\$$/; +const ERROR_CONTEXT_WIDTH = 30; // Transform the expected output to set of tokens function tokenize(text: string): Piece[] { 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 to = from + 30; - throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`) + const to = from + ERROR_CONTEXT_WIDTH; + throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`); } 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 expr = r(pieces); - if (!expr.test(source)) { + const {regexp, groups} = buildMatcher(pieces); + const matches = source.match(regexp); + if (matches === null) { let last: number = 0; for (let i = 1; i < pieces.length; i++) { - const t = r(pieces.slice(0, i)); - const m = source.match(t); + const {regexp} = buildMatcher(pieces.slice(0, i)); + const m = source.match(regexp); const expectedPiece = pieces[i - 1] == IDENTIFIER ? '' : pieces[i - 1]; if (!m) { fail( @@ -85,11 +88,42 @@ export function expectEmit(source: string, expected: string, description: string } fail( `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]/; -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} { const results: string[] = []; let first = true; let group = 0; @@ -116,7 +150,10 @@ function r(pieces: (string | RegExp)[]): RegExp { results.push('(?:' + piece.source + ')'); } } - return new RegExp(results.join('')); + return { + regexp: new RegExp(results.join('')), + groups, + }; } function doCompile( @@ -164,8 +201,6 @@ function doCompile( new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector); - - // Create the TypeScript program const sourceFiles = program.getSourceFiles().map(sf => sf.fileName); diff --git a/packages/compiler/test/render3/mock_compiler_spec.ts b/packages/compiler/test/render3/mock_compiler_spec.ts index 4c7e44145c..e3bb9bea14 100644 --- a/packages/compiler/test/render3/mock_compiler_spec.ts +++ b/packages/compiler/test/render3/mock_compiler_spec.ts @@ -36,7 +36,7 @@ describe('mock_compiler', () => { 'hello.module.ts': ` import {NgModule} from '@angular/core'; import {HelloComponent} from './hello.component'; - + @NgModule({declarations: [HelloComponent]}) export class HelloModule {} ` @@ -68,7 +68,7 @@ describe('mock_compiler', () => { 'hello.module.ts': ` import {NgModule} from '@angular/core'; import {HelloComponent} from './hello.component'; - + @NgModule({declarations: [HelloComponent]}) export class HelloModule {} ` @@ -101,7 +101,7 @@ describe('mock_compiler', () => { 'hello.module.ts': ` import {NgModule} from '@angular/core'; import {HelloComponent} from './hello.component'; - + @NgModule({declarations: [HelloComponent]}) export class HelloModule {} ` @@ -131,7 +131,7 @@ describe('mock_compiler', () => { 'hello.module.ts': ` import {NgModule} from '@angular/core'; import {HelloComponent} from './hello.component'; - + @NgModule({declarations: [HelloComponent]}) export class HelloModule {} ` @@ -151,4 +151,38 @@ describe('mock_compiler', () => { result.source, '$ctx$.$name$ … $ctx$.$name$.length', '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\)\//); + }); + }); \ No newline at end of file