| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | import {escapeRegExp} from '@angular/compiler/src/util'; | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  | import {arrayToMockDir, MockCompilerHost, MockData, MockDirectory, toMockFileArray} from '@angular/compiler/test/aot/test_util'; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | import * as ts from 'typescript'; | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-22 15:42:33 +01:00
										 |  |  | import {NgCompilerOptions} from '../../../src/ngtsc/core/api'; | 
					
						
							|  |  |  | import {NodeJSFileSystem, setFileSystem} from '../../../src/ngtsc/file_system'; | 
					
						
							|  |  |  | import {NgtscProgram} from '../../../src/ngtsc/program'; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-17 18:49:21 -07:00
										 |  |  | const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | const OPERATOR = | 
					
						
							| 
									
										
										
										
											2019-11-15 16:25:59 +00:00
										 |  |  |     /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\.|`|\\'/; | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  | const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"/; | 
					
						
							| 
									
										
										
										
											2019-11-15 16:25:59 +00:00
										 |  |  | const BACKTICK_STRING = /\\`(([\s\S]*?)(\$\{[^}]*?\})?)*?[^\\]\\`/; | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  | const BACKTICK_INTERPOLATION = /(\$\{[^}]*\})/; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | const NUMBER = /\d+/; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ELLIPSIS = '…'; | 
					
						
							|  |  |  | const TOKEN = new RegExp( | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |     `\\s*((${IDENTIFIER.source})|(${BACKTICK_STRING.source})|(${OPERATOR.source})|(${ | 
					
						
							|  |  |  |         STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`,
 | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |     'y'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  | type Piece = string|RegExp; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | const SKIP = /(?:.|\n|\r)*/; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ERROR_CONTEXT_WIDTH = 30; | 
					
						
							|  |  |  | // Transform the expected output to set of tokens
 | 
					
						
							|  |  |  | function tokenize(text: string): Piece[] { | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  |   // TOKEN.lastIndex is stateful so we cache the `lastIndex` and restore it at the end of the call.
 | 
					
						
							|  |  |  |   const lastIndex = TOKEN.lastIndex; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   TOKEN.lastIndex = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let match: RegExpMatchArray|null; | 
					
						
							| 
									
										
										
										
											2019-05-21 21:59:53 +02:00
										 |  |  |   let tokenizedTextEnd = 0; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   const pieces: Piece[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   while ((match = TOKEN.exec(text)) !== null) { | 
					
						
							| 
									
										
										
										
											2019-05-21 21:59:53 +02:00
										 |  |  |     const [fullMatch, token] = match; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |     if (token === 'IDENT') { | 
					
						
							|  |  |  |       pieces.push(IDENTIFIER); | 
					
						
							|  |  |  |     } else if (token === ELLIPSIS) { | 
					
						
							|  |  |  |       pieces.push(SKIP); | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  |     } else if (match = BACKTICK_STRING.exec(token)) { | 
					
						
							|  |  |  |       pieces.push(...tokenizeBackTickString(token)); | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |     } else { | 
					
						
							|  |  |  |       pieces.push(token); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-05-21 21:59:53 +02:00
										 |  |  |     tokenizedTextEnd += fullMatch.length; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-21 21:59:53 +02:00
										 |  |  |   if (pieces.length === 0 || tokenizedTextEnd < text.length) { | 
					
						
							|  |  |  |     // The new token that could not be found is located after the
 | 
					
						
							|  |  |  |     // last tokenized character.
 | 
					
						
							|  |  |  |     const from = tokenizedTextEnd; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |     const to = from + ERROR_CONTEXT_WIDTH; | 
					
						
							| 
									
										
										
										
											2019-05-21 21:59:53 +02:00
										 |  |  |     throw Error( | 
					
						
							|  |  |  |         `Invalid test, no token found for "${text[tokenizedTextEnd]}" ` + | 
					
						
							|  |  |  |         `(context = '${text.substr(from, to)}...'`); | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  |   // Reset the lastIndex in case we are in a recursive `tokenize()` call.
 | 
					
						
							|  |  |  |   TOKEN.lastIndex = lastIndex; | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return pieces; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Back-ticks are escaped as "\`" so we must strip the backslashes. | 
					
						
							|  |  |  |  * Also the string will likely contain interpolations and if an interpolation holds an | 
					
						
							|  |  |  |  * identifier we will need to match that later. So tokenize the interpolation too! | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function tokenizeBackTickString(str: string): Piece[] { | 
					
						
							|  |  |  |   const pieces: Piece[] = ['`']; | 
					
						
							| 
									
										
										
										
											2019-11-15 16:25:59 +00:00
										 |  |  |   // Unescape backticks that are inside the backtick string
 | 
					
						
							|  |  |  |   // (we had to double escape them in the test string so they didn't look like string markers)
 | 
					
						
							| 
									
										
										
										
											2019-11-26 14:03:19 +00:00
										 |  |  |   str = str.replace(/\\\\\\`/g, '\\`'); | 
					
						
							| 
									
										
										
										
											2019-07-30 18:02:17 +01:00
										 |  |  |   const backTickPieces = str.slice(2, -2).split(BACKTICK_INTERPOLATION); | 
					
						
							|  |  |  |   backTickPieces.forEach((backTickPiece) => { | 
					
						
							|  |  |  |     if (BACKTICK_INTERPOLATION.test(backTickPiece)) { | 
					
						
							|  |  |  |       // An interpolation so tokenize this expression
 | 
					
						
							|  |  |  |       pieces.push(...tokenize(backTickPiece)); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       // Not an interpolation so just add it as a piece
 | 
					
						
							|  |  |  |       pieces.push(backTickPiece); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   pieces.push('`'); | 
					
						
							|  |  |  |   return pieces; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | export function expectEmit( | 
					
						
							|  |  |  |     source: string, expected: string, description: string, | 
					
						
							|  |  |  |     assertIdentifiers?: {[name: string]: RegExp}) { | 
					
						
							|  |  |  |   // turns `// ...` into `…`
 | 
					
						
							|  |  |  |   // remove `// TODO` comment lines
 | 
					
						
							|  |  |  |   expected = expected.replace(/\/\/\s*\.\.\./g, ELLIPSIS).replace(/\/\/\s*TODO.*?\n/g, ''); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const pieces = tokenize(expected); | 
					
						
							|  |  |  |   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 {regexp} = buildMatcher(pieces.slice(0, i)); | 
					
						
							|  |  |  |       const m = source.match(regexp); | 
					
						
							|  |  |  |       const expectedPiece = pieces[i - 1] == IDENTIFIER ? '<IDENT>' : pieces[i - 1]; | 
					
						
							|  |  |  |       if (!m) { | 
					
						
							| 
									
										
										
										
											2018-08-03 15:32:08 -07:00
										 |  |  |         // display at most `contextLength` characters of the line preceding the error location
 | 
					
						
							|  |  |  |         const contextLength = 50; | 
					
						
							|  |  |  |         const fullContext = source.substring(source.lastIndexOf('\n', last) + 1, last); | 
					
						
							|  |  |  |         const context = fullContext.length > contextLength ? | 
					
						
							|  |  |  |             `...${fullContext.substr(-contextLength)}` : | 
					
						
							|  |  |  |             fullContext; | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |         fail(`${description}: Failed to find "${expectedPiece}" after "${context}" in:\n'${ | 
					
						
							|  |  |  |             source.substr(0, last)}[<---HERE expected "${expectedPiece}"]${source.substr(last)}'`);
 | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |         return; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         last = (m.index || 0) + m[0].length; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     fail( | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |         `Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${ | 
					
						
							|  |  |  |             source}`);
 | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   } 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)) { | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |             throw Error(`${description}: The matching identifier "${id}" is "${ | 
					
						
							|  |  |  |                 name}" which doesn't match ${regexp}`);
 | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const IDENT_LIKE = /^[a-z][A-Z]/; | 
					
						
							|  |  |  | 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. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  | function buildMatcher(pieces: (string|RegExp)[]): {regexp: RegExp, groups: Map<string, number>} { | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   const results: string[] = []; | 
					
						
							|  |  |  |   let first = true; | 
					
						
							|  |  |  |   let group = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const groups = new Map<string, number>(); | 
					
						
							|  |  |  |   for (const piece of pieces) { | 
					
						
							|  |  |  |     if (!first) | 
					
						
							|  |  |  |       results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`); | 
					
						
							|  |  |  |     first = false; | 
					
						
							|  |  |  |     if (typeof piece === 'string') { | 
					
						
							|  |  |  |       if (MATCHING_IDENT.test(piece)) { | 
					
						
							|  |  |  |         const matchGroup = groups.get(piece); | 
					
						
							|  |  |  |         if (!matchGroup) { | 
					
						
							|  |  |  |           results.push('(' + IDENTIFIER.source + ')'); | 
					
						
							|  |  |  |           const newGroup = ++group; | 
					
						
							|  |  |  |           groups.set(piece, newGroup); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           results.push(`\\${matchGroup}`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         results.push(escapeRegExp(piece)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       results.push('(?:' + piece.source + ')'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     regexp: new RegExp(results.join('')), | 
					
						
							|  |  |  |     groups, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-30 23:04:09 +01:00
										 |  |  | export function compileFiles( | 
					
						
							| 
									
										
										
										
											2020-09-22 15:42:33 +01:00
										 |  |  |     data: MockDirectory, angularFiles: MockData, options: NgCompilerOptions = {}): { | 
					
						
							| 
									
										
										
										
											2020-10-30 23:04:09 +01:00
										 |  |  |   fileName: string; source: string, | 
					
						
							|  |  |  | }[] { | 
					
						
							| 
									
										
										
										
											2019-06-06 20:22:32 +01:00
										 |  |  |   setFileSystem(new NodeJSFileSystem()); | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |   const testFiles = toMockFileArray(data); | 
					
						
							|  |  |  |   const scripts = testFiles.map(entry => entry.fileName); | 
					
						
							|  |  |  |   const angularFilesArray = toMockFileArray(angularFiles); | 
					
						
							|  |  |  |   const files = arrayToMockDir([...testFiles, ...angularFilesArray]); | 
					
						
							|  |  |  |   const mockCompilerHost = new MockCompilerHost(scripts, files); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const program = new NgtscProgram( | 
					
						
							|  |  |  |       scripts, { | 
					
						
							|  |  |  |         target: ts.ScriptTarget.ES2015, | 
					
						
							|  |  |  |         module: ts.ModuleKind.ES2015, | 
					
						
							| 
									
										
										
										
											2019-12-03 08:36:38 +00:00
										 |  |  |         moduleResolution: ts.ModuleResolutionKind.NodeJs, | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |         enableI18nLegacyMessageIdFormat: false, | 
					
						
							|  |  |  |         ...options, | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  |       }, | 
					
						
							|  |  |  |       mockCompilerHost); | 
					
						
							|  |  |  |   program.emit(); | 
					
						
							| 
									
										
										
										
											2020-10-30 23:04:09 +01:00
										 |  |  |   return scripts.map(script => { | 
					
						
							|  |  |  |     const fileName = script.replace(/\.ts$/, '.js'); | 
					
						
							|  |  |  |     const source = mockCompilerHost.readFile(fileName); | 
					
						
							|  |  |  |     return {fileName, source}; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function doCompile(data: MockDirectory, angularFiles: MockData, options: NgCompilerOptions = {}): { | 
					
						
							|  |  |  |   source: string, | 
					
						
							|  |  |  | } { | 
					
						
							|  |  |  |   const scripts = compileFiles(data, angularFiles, options); | 
					
						
							|  |  |  |   const source = scripts.map(script => script.source).join('\n'); | 
					
						
							| 
									
										
										
										
											2018-07-12 15:10:55 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return {source}; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2020-09-22 15:42:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | export type CompileFn = typeof doCompile; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * The actual compile function that will be used to compile the test code. | 
					
						
							|  |  |  |  * This can be updated by a test bootstrap script to provide an alternative compile function. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export let compile: CompileFn = doCompile; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Update the `compile` exported function to use a new implementation. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export function setCompileFn(compileFn: CompileFn) { | 
					
						
							|  |  |  |   compile = compileFn; | 
					
						
							|  |  |  | } |