Skip to content

Commit

Permalink
test(compiler): allow asserting matching identifier names
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed Mar 20, 2018
1 parent 3e057ad commit 88db204
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 18 deletions.
63 changes: 49 additions & 14 deletions packages/compiler/test/render3/mock_compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+/;

Expand All @@ -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;
Expand All @@ -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 ? '<IDENT>' : pieces[i - 1];
if (!m) {
fail(
Expand All @@ -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<string, number>} {
const results: string[] = [];
let first = true;
let group = 0;
Expand All @@ -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(
Expand Down Expand Up @@ -166,8 +203,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);

Expand Down
42 changes: 38 additions & 4 deletions packages/compiler/test/render3/mock_compiler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
`
Expand Down Expand Up @@ -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 {}
`
Expand Down Expand Up @@ -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 {}
`
Expand Down Expand Up @@ -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 {}
`
Expand All @@ -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\)\//);
});

});

0 comments on commit 88db204

Please sign in to comment.