
* don't reexport symbols that the user already reexported * never reexport symbols that are part of arguments of non simple function calls Fixes #19883 PR Close #19884
379 lines
14 KiB
TypeScript
379 lines
14 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 {createLoweredSymbol, isLoweredSymbol} from '@angular/compiler';
|
|
import * as ts from 'typescript';
|
|
|
|
import {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '../metadata/index';
|
|
|
|
export interface LoweringRequest {
|
|
kind: ts.SyntaxKind;
|
|
location: number;
|
|
end: number;
|
|
name: string;
|
|
}
|
|
|
|
export type RequestLocationMap = Map<number, LoweringRequest>;
|
|
|
|
const enum DeclarationOrder { BeforeStmt, AfterStmt }
|
|
|
|
interface Declaration {
|
|
name: string;
|
|
node: ts.Node;
|
|
order: DeclarationOrder;
|
|
}
|
|
|
|
interface DeclarationInsert {
|
|
declarations: Declaration[];
|
|
relativeTo: ts.Node;
|
|
}
|
|
|
|
function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> {
|
|
return new Map(items.map<[K, T]>(i => [select(i), i]));
|
|
}
|
|
|
|
// We will never lower expressions in a nested lexical scope so avoid entering them.
|
|
// This also avoids a bug in TypeScript 2.3 where the lexical scopes get out of sync
|
|
// when using visitEachChild.
|
|
function isLexicalScope(node: ts.Node): boolean {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.ArrowFunction:
|
|
case ts.SyntaxKind.FunctionExpression:
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
case ts.SyntaxKind.ClassExpression:
|
|
case ts.SyntaxKind.ClassDeclaration:
|
|
case ts.SyntaxKind.FunctionType:
|
|
case ts.SyntaxKind.TypeLiteral:
|
|
case ts.SyntaxKind.ArrayType:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function transformSourceFile(
|
|
sourceFile: ts.SourceFile, requests: RequestLocationMap,
|
|
context: ts.TransformationContext): ts.SourceFile {
|
|
const inserts: DeclarationInsert[] = [];
|
|
|
|
// Calculate the range of interesting locations. The transform will only visit nodes in this
|
|
// range to improve the performance on large files.
|
|
const locations = Array.from(requests.keys());
|
|
const min = Math.min(...locations);
|
|
const max = Math.max(...locations);
|
|
|
|
// Visit nodes matching the request and synthetic nodes added by tsickle
|
|
function shouldVisit(pos: number, end: number): boolean {
|
|
return (pos <= max && end >= min) || pos == -1;
|
|
}
|
|
|
|
function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile {
|
|
function topLevelStatement(node: ts.Statement): ts.Statement {
|
|
const declarations: Declaration[] = [];
|
|
|
|
function visitNode(node: ts.Node): ts.Node {
|
|
// Get the original node before tsickle
|
|
const {pos, end, kind, parent: originalParent} = ts.getOriginalNode(node);
|
|
const nodeRequest = requests.get(pos);
|
|
if (nodeRequest && nodeRequest.kind == kind && nodeRequest.end == end) {
|
|
// This node is requested to be rewritten as a reference to the exported name.
|
|
if (originalParent && originalParent.kind === ts.SyntaxKind.VariableDeclaration) {
|
|
// As the value represents the whole initializer of a variable declaration,
|
|
// just refer to that variable. This e.g. helps to preserve closure comments
|
|
// at the right place.
|
|
const varParent = originalParent as ts.VariableDeclaration;
|
|
if (varParent.name.kind === ts.SyntaxKind.Identifier) {
|
|
const varName = varParent.name.text;
|
|
const exportName = nodeRequest.name;
|
|
declarations.push({
|
|
name: exportName,
|
|
node: ts.createIdentifier(varName),
|
|
order: DeclarationOrder.AfterStmt
|
|
});
|
|
return node;
|
|
}
|
|
}
|
|
// Record that the node needs to be moved to an exported variable with the given name
|
|
const exportName = nodeRequest.name;
|
|
declarations.push({name: exportName, node, order: DeclarationOrder.BeforeStmt});
|
|
return ts.createIdentifier(exportName);
|
|
}
|
|
let result = node;
|
|
if (shouldVisit(pos, end) && !isLexicalScope(node)) {
|
|
result = ts.visitEachChild(node, visitNode, context);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Get the original node before tsickle
|
|
const {pos, end} = ts.getOriginalNode(node);
|
|
let resultStmt: ts.Statement;
|
|
if (shouldVisit(pos, end)) {
|
|
resultStmt = ts.visitEachChild(node, visitNode, context);
|
|
} else {
|
|
resultStmt = node;
|
|
}
|
|
|
|
if (declarations.length) {
|
|
inserts.push({relativeTo: resultStmt, declarations});
|
|
}
|
|
return resultStmt;
|
|
}
|
|
|
|
let newStatements = sourceFile.statements.map(topLevelStatement);
|
|
|
|
if (inserts.length) {
|
|
// Insert the declarations relative to the rewritten statement that references them.
|
|
const insertMap = toMap(inserts, i => i.relativeTo);
|
|
const tmpStatements: ts.Statement[] = [];
|
|
newStatements.forEach(statement => {
|
|
const insert = insertMap.get(statement);
|
|
if (insert) {
|
|
const before = insert.declarations.filter(d => d.order === DeclarationOrder.BeforeStmt);
|
|
if (before.length) {
|
|
tmpStatements.push(createVariableStatementForDeclarations(before));
|
|
}
|
|
tmpStatements.push(statement);
|
|
const after = insert.declarations.filter(d => d.order === DeclarationOrder.AfterStmt);
|
|
if (after.length) {
|
|
tmpStatements.push(createVariableStatementForDeclarations(after));
|
|
}
|
|
} else {
|
|
tmpStatements.push(statement);
|
|
}
|
|
});
|
|
|
|
// Insert an exports clause to export the declarations
|
|
tmpStatements.push(ts.createExportDeclaration(
|
|
/* decorators */ undefined,
|
|
/* modifiers */ undefined,
|
|
ts.createNamedExports(
|
|
inserts
|
|
.reduce(
|
|
(accumulator, insert) => [...accumulator, ...insert.declarations],
|
|
[] as Declaration[])
|
|
.map(
|
|
declaration => ts.createExportSpecifier(
|
|
/* propertyName */ undefined, declaration.name)))));
|
|
|
|
newStatements = tmpStatements;
|
|
}
|
|
// Note: We cannot use ts.updateSourcefile here as
|
|
// it does not work well with decorators.
|
|
// See https://github.com/Microsoft/TypeScript/issues/17384
|
|
const newSf = ts.getMutableClone(sourceFile);
|
|
if (!(sourceFile.flags & ts.NodeFlags.Synthesized)) {
|
|
newSf.flags &= ~ts.NodeFlags.Synthesized;
|
|
}
|
|
newSf.statements = ts.setTextRange(ts.createNodeArray(newStatements), sourceFile.statements);
|
|
return newSf;
|
|
}
|
|
|
|
return visitSourceFile(sourceFile);
|
|
}
|
|
|
|
function createVariableStatementForDeclarations(declarations: Declaration[]): ts.VariableStatement {
|
|
const varDecls = declarations.map(
|
|
i => ts.createVariableDeclaration(i.name, /* type */ undefined, i.node as ts.Expression));
|
|
return ts.createVariableStatement(
|
|
/* modifiers */ undefined, ts.createVariableDeclarationList(varDecls, ts.NodeFlags.Const));
|
|
}
|
|
|
|
export function getExpressionLoweringTransformFactory(
|
|
requestsMap: RequestsMap, program: ts.Program): (context: ts.TransformationContext) =>
|
|
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
|
// Return the factory
|
|
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
|
|
// We need to use the original SourceFile for reading metadata, and not the transformed one.
|
|
const requests = requestsMap.getRequests(program.getSourceFile(sourceFile.fileName));
|
|
if (requests && requests.size) {
|
|
return transformSourceFile(sourceFile, requests, context);
|
|
}
|
|
return sourceFile;
|
|
};
|
|
}
|
|
|
|
export interface RequestsMap { getRequests(sourceFile: ts.SourceFile): RequestLocationMap; }
|
|
|
|
interface MetadataAndLoweringRequests {
|
|
metadata: ModuleMetadata|undefined;
|
|
requests: RequestLocationMap;
|
|
}
|
|
|
|
function shouldLower(node: ts.Node | undefined): boolean {
|
|
if (node) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.SourceFile:
|
|
case ts.SyntaxKind.Decorator:
|
|
// Lower expressions that are local to the module scope or
|
|
// in a decorator.
|
|
return true;
|
|
case ts.SyntaxKind.ClassDeclaration:
|
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
case ts.SyntaxKind.EnumDeclaration:
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
// Don't lower expressions in a declaration.
|
|
return false;
|
|
case ts.SyntaxKind.VariableDeclaration:
|
|
// Avoid lowering expressions already in an exported variable declaration
|
|
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) == 0;
|
|
}
|
|
return shouldLower(node.parent);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isPrimitive(value: any): boolean {
|
|
return Object(value) !== value;
|
|
}
|
|
|
|
function isRewritten(value: any): boolean {
|
|
return isMetadataGlobalReferenceExpression(value) && isLoweredSymbol(value.name);
|
|
}
|
|
|
|
function isLiteralFieldNamed(node: ts.Node, names: Set<string>): boolean {
|
|
if (node.parent && node.parent.kind == ts.SyntaxKind.PropertyAssignment) {
|
|
const property = node.parent as ts.PropertyAssignment;
|
|
if (property.parent && property.parent.kind == ts.SyntaxKind.ObjectLiteralExpression &&
|
|
property.name && property.name.kind == ts.SyntaxKind.Identifier) {
|
|
const propertyName = property.name as ts.Identifier;
|
|
return names.has(propertyName.text);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const LOWERABLE_FIELD_NAMES = new Set(['useValue', 'useFactory', 'data']);
|
|
|
|
export class LowerMetadataCache implements RequestsMap {
|
|
private collector: MetadataCollector;
|
|
private metadataCache = new Map<string, MetadataAndLoweringRequests>();
|
|
|
|
constructor(options: CollectorOptions, private strict?: boolean) {
|
|
this.collector = new MetadataCollector(options);
|
|
}
|
|
|
|
getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined {
|
|
return this.ensureMetadataAndRequests(sourceFile).metadata;
|
|
}
|
|
|
|
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
|
|
return this.ensureMetadataAndRequests(sourceFile).requests;
|
|
}
|
|
|
|
private ensureMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
|
|
let result = this.metadataCache.get(sourceFile.fileName);
|
|
if (!result) {
|
|
result = this.getMetadataAndRequests(sourceFile);
|
|
this.metadataCache.set(sourceFile.fileName, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private getMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
|
|
let identNumber = 0;
|
|
const freshIdent = () => createLoweredSymbol(identNumber++);
|
|
const requests = new Map<number, LoweringRequest>();
|
|
|
|
const isExportedSymbol = (() => {
|
|
let exportTable: Set<string>;
|
|
return (node: ts.Node) => {
|
|
if (node.kind == ts.SyntaxKind.Identifier) {
|
|
const ident = node as ts.Identifier;
|
|
|
|
if (!exportTable) {
|
|
exportTable = createExportTableFor(sourceFile);
|
|
}
|
|
return exportTable.has(ident.text);
|
|
}
|
|
return false;
|
|
};
|
|
})();
|
|
|
|
const isExportedPropertyAccess = (node: ts.Node) => {
|
|
if (node.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
|
const pae = node as ts.PropertyAccessExpression;
|
|
if (isExportedSymbol(pae.expression)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
const replaceNode = (node: ts.Node) => {
|
|
const name = freshIdent();
|
|
requests.set(node.pos, {name, kind: node.kind, location: node.pos, end: node.end});
|
|
return {__symbolic: 'reference', name};
|
|
};
|
|
|
|
const substituteExpression = (value: MetadataValue, node: ts.Node): MetadataValue => {
|
|
if (!isPrimitive(value) && !isRewritten(value)) {
|
|
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
|
|
node.kind === ts.SyntaxKind.FunctionExpression) &&
|
|
shouldLower(node)) {
|
|
return replaceNode(node);
|
|
}
|
|
if (isLiteralFieldNamed(node, LOWERABLE_FIELD_NAMES) && shouldLower(node) &&
|
|
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
|
|
return replaceNode(node);
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
// Do not validate or lower metadata in a declaration file. Declaration files are requested
|
|
// when we need to update the version of the metadata to add informatoin that might be missing
|
|
// in the out-of-date version that can be recovered from the .d.ts file.
|
|
const declarationFile = sourceFile.isDeclarationFile;
|
|
|
|
const metadata = this.collector.getMetadata(
|
|
sourceFile, this.strict && !declarationFile,
|
|
declarationFile ? undefined : substituteExpression);
|
|
|
|
return {metadata, requests};
|
|
}
|
|
}
|
|
|
|
function createExportTableFor(sourceFile: ts.SourceFile): Set<string> {
|
|
const exportTable = new Set<string>();
|
|
// Lazily collect all the exports from the source file
|
|
ts.forEachChild(sourceFile, function scan(node) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.ClassDeclaration:
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) != 0) {
|
|
const classDeclaration =
|
|
node as(ts.ClassDeclaration | ts.FunctionDeclaration | ts.InterfaceDeclaration);
|
|
const name = classDeclaration.name;
|
|
if (name) exportTable.add(name.text);
|
|
}
|
|
break;
|
|
case ts.SyntaxKind.VariableStatement:
|
|
const variableStatement = node as ts.VariableStatement;
|
|
for (const declaration of variableStatement.declarationList.declarations) {
|
|
scan(declaration);
|
|
}
|
|
break;
|
|
case ts.SyntaxKind.VariableDeclaration:
|
|
const variableDeclaration = node as ts.VariableDeclaration;
|
|
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) != 0 &&
|
|
variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
|
|
const name = variableDeclaration.name as ts.Identifier;
|
|
exportTable.add(name.text);
|
|
}
|
|
break;
|
|
case ts.SyntaxKind.ExportDeclaration:
|
|
const exportDeclaration = node as ts.ExportDeclaration;
|
|
const {moduleSpecifier, exportClause} = exportDeclaration;
|
|
if (!moduleSpecifier && exportClause) {
|
|
exportClause.elements.forEach(spec => { exportTable.add(spec.name.text); });
|
|
}
|
|
}
|
|
});
|
|
return exportTable;
|
|
}
|