c0b383590e
In 7cb8396, we improved the symbol test to remove suffixes from the output, so "CIRCULAR$1" became "CIRCULAR". However, the logic that checked for extra symbols depended on each symbol being unique. If multiple symbols with the same name were added (e.g. pulled in from separate files), they would be added to the map as "extras", even if they were marked as expected in the golden file. This commit updates the symbol checking logic to take multiple symbols with the same name into account. Closes #28406 PR Close #28459
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* 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 * as ts from 'typescript';
|
|
|
|
|
|
export interface Symbol { name: string; }
|
|
|
|
export class SymbolExtractor {
|
|
public actual: Symbol[];
|
|
|
|
static symbolSort(a: Symbol, b: Symbol): number {
|
|
return a.name == b.name ? 0 : a.name < b.name ? -1 : 1;
|
|
}
|
|
|
|
static parse(path: string, contents: string): Symbol[] {
|
|
const symbols: Symbol[] = [];
|
|
const source: ts.SourceFile = ts.createSourceFile(path, contents, ts.ScriptTarget.Latest, true);
|
|
let fnRecurseDepth = 0;
|
|
function visitor(child: ts.Node) {
|
|
// Left for easier debugging.
|
|
// console.log('>>>', ts.SyntaxKind[child.kind]);
|
|
switch (child.kind) {
|
|
case ts.SyntaxKind.FunctionExpression:
|
|
fnRecurseDepth++;
|
|
if (fnRecurseDepth <= 1) {
|
|
ts.forEachChild(child, visitor);
|
|
}
|
|
fnRecurseDepth--;
|
|
break;
|
|
case ts.SyntaxKind.SourceFile:
|
|
case ts.SyntaxKind.VariableStatement:
|
|
case ts.SyntaxKind.VariableDeclarationList:
|
|
case ts.SyntaxKind.ExpressionStatement:
|
|
case ts.SyntaxKind.CallExpression:
|
|
case ts.SyntaxKind.ParenthesizedExpression:
|
|
case ts.SyntaxKind.Block:
|
|
case ts.SyntaxKind.PrefixUnaryExpression:
|
|
ts.forEachChild(child, visitor);
|
|
break;
|
|
case ts.SyntaxKind.VariableDeclaration:
|
|
const varDecl = child as ts.VariableDeclaration;
|
|
if (varDecl.initializer && fnRecurseDepth !== 0) {
|
|
symbols.push({name: stripSuffix(varDecl.name.getText())});
|
|
}
|
|
if (fnRecurseDepth == 0 && isRollupExportSymbol(varDecl)) {
|
|
ts.forEachChild(child, visitor);
|
|
}
|
|
break;
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
const funcDecl = child as ts.FunctionDeclaration;
|
|
funcDecl.name && symbols.push({name: stripSuffix(funcDecl.name.getText())});
|
|
break;
|
|
default:
|
|
// Left for easier debugging.
|
|
// console.log('###', ts.SyntaxKind[child.kind], child.getText());
|
|
}
|
|
}
|
|
visitor(source);
|
|
symbols.sort(SymbolExtractor.symbolSort);
|
|
return symbols;
|
|
}
|
|
|
|
static diff(actual: Symbol[], expected: string|((Symbol | string)[])): {[name: string]: number} {
|
|
if (typeof expected == 'string') {
|
|
expected = JSON.parse(expected);
|
|
}
|
|
const diff: {[name: string]: number} = {};
|
|
|
|
// All symbols in the golden file start out with a count corresponding to the number of symbols
|
|
// with that name. Once they are matched with symbols in the actual output, the count should
|
|
// even out to 0.
|
|
(expected as(Symbol | string)[]).forEach((nameOrSymbol) => {
|
|
const symbolName = typeof nameOrSymbol == 'string' ? nameOrSymbol : nameOrSymbol.name;
|
|
diff[symbolName] = (diff[symbolName] || 0) + 1;
|
|
});
|
|
|
|
actual.forEach((s) => {
|
|
if (diff[s.name] === 1) {
|
|
delete diff[s.name];
|
|
} else {
|
|
diff[s.name] = (diff[s.name] || 0) - 1;
|
|
}
|
|
});
|
|
return diff;
|
|
}
|
|
|
|
|
|
constructor(private path: string, private contents: string) {
|
|
this.actual = SymbolExtractor.parse(path, contents);
|
|
}
|
|
|
|
expect(expectedSymbols: (string|Symbol)[]) {
|
|
expect(SymbolExtractor.diff(this.actual, expectedSymbols)).toEqual({});
|
|
}
|
|
|
|
compareAndPrintError(goldenFilePath: string, expected: string|((Symbol | string)[])): boolean {
|
|
let passed = true;
|
|
const diff = SymbolExtractor.diff(this.actual, expected);
|
|
Object.keys(diff).forEach((key) => {
|
|
if (passed) {
|
|
console.error(`Expected symbols in '${this.path}' did not match gold file.`);
|
|
passed = false;
|
|
}
|
|
const missingOrExtra = diff[key] > 0 ? 'extra' : 'missing';
|
|
const count = Math.abs(diff[key]);
|
|
console.error(` Symbol: ${key} => ${count} ${missingOrExtra} in golden file.`);
|
|
});
|
|
|
|
return passed;
|
|
}
|
|
}
|
|
|
|
function stripSuffix(text: string): string {
|
|
const index = text.lastIndexOf('$');
|
|
return index > -1 ? text.substring(0, index) : text;
|
|
}
|
|
|
|
/**
|
|
* Detects if VariableDeclarationList is format `var ..., bundle = function(){}()`;
|
|
*
|
|
* Rollup produces this format when it wants to export symbols from a bundle.
|
|
* @param child
|
|
*/
|
|
function isRollupExportSymbol(decl: ts.VariableDeclaration): boolean {
|
|
return !!(decl.initializer && decl.initializer.kind == ts.SyntaxKind.CallExpression) &&
|
|
ts.isIdentifier(decl.name) && decl.name.text === 'bundle';
|
|
}
|