feat(compiler-cli): reflect static methods added to classes in metadata (#21926)

PR Close #21926
This commit is contained in:
Chuck Jazdzewski 2018-01-30 17:17:54 -08:00 committed by Alex Rickabaugh
parent 1aa2947f70
commit eb8ddd2983
8 changed files with 252 additions and 61 deletions

View File

@ -76,6 +76,9 @@ export class MetadataCollector {
}
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T {
if (composedSubstituter) {
entry = composedSubstituter(entry as MetadataValue, node) as T;
}
return recordMapEntry(entry, node, nodeMap, sourceFile);
}

View File

@ -10,6 +10,7 @@ import {createLoweredSymbol, isLoweredSymbol} from '@angular/compiler';
import * as ts from 'typescript';
import {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '../metadata/index';
import {MetadataCache, MetadataTransformer, ValueTransform} from './metadata_cache';
export interface LoweringRequest {
kind: ts.SyntaxKind;
@ -249,35 +250,39 @@ function isLiteralFieldNamed(node: ts.Node, names: Set<string>): boolean {
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;
}
export class LowerMetadataTransform implements RequestsMap, MetadataTransformer {
private cache: MetadataCache;
private requests = new Map<string, RequestLocationMap>();
// RequestMap
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
return this.ensureMetadataAndRequests(sourceFile).requests;
}
private ensureMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
let result = this.metadataCache.get(sourceFile.fileName);
let result = this.requests.get(sourceFile.fileName);
if (!result) {
result = this.getMetadataAndRequests(sourceFile);
this.metadataCache.set(sourceFile.fileName, result);
// Force the metadata for this source file to be collected which
// will recursively call start() populating the request map;
this.cache.getMetadata(sourceFile);
// If we still don't have the requested metadata, the file is not a module
// or is a declaration file so return an empty map.
result = this.requests.get(sourceFile.fileName) || new Map<number, LoweringRequest>();
}
return result;
}
private getMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
// MetadataTransformer
connect(cache: MetadataCache): void { this.cache = cache; }
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
let identNumber = 0;
const freshIdent = () => createLoweredSymbol(identNumber++);
const requests = new Map<number, LoweringRequest>();
this.requests.set(sourceFile.fileName, requests);
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 isExportedSymbol = (() => {
let exportTable: Set<string>;
@ -303,13 +308,8 @@ export class LowerMetadataCache implements RequestsMap {
}
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 => {
return (value: MetadataValue, node: ts.Node): MetadataValue => {
if (!isPrimitive(value) && !isRewritten(value)) {
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) &&
@ -323,18 +323,6 @@ export class LowerMetadataCache implements RequestsMap {
}
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 information that might be missing
// in the out-of-date version that can be recovered from the .d.ts file.
const declarationFile = sourceFile.isDeclarationFile;
const moduleFile = ts.isExternalModule(sourceFile);
const metadata = this.collector.getMetadata(
sourceFile, this.strict && !declarationFile,
moduleFile && !declarationFile ? substituteExpression : undefined);
return {metadata, requests};
}
}

View File

@ -0,0 +1,66 @@
/**
* @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';
import {MetadataCollector, MetadataValue, ModuleMetadata} from '../metadata/index';
import {MetadataProvider} from './compiler_host';
export type ValueTransform = (value: MetadataValue, node: ts.Node) => MetadataValue;
export interface MetadataTransformer {
connect?(cache: MetadataCache): void;
start(sourceFile: ts.SourceFile): ValueTransform|undefined;
}
/**
* Cache, and potentially transform, metadata as it is being collected.
*/
export class MetadataCache implements MetadataProvider {
private metadataCache = new Map<string, ModuleMetadata|undefined>();
constructor(
private collector: MetadataCollector, private strict: boolean,
private transformers: MetadataTransformer[]) {
for (let transformer of transformers) {
if (transformer.connect) {
transformer.connect(this);
}
}
}
getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined {
if (this.metadataCache.has(sourceFile.fileName)) {
return this.metadataCache.get(sourceFile.fileName);
}
let substitute: ValueTransform|undefined = undefined;
// Only process transformers on modules that are not declaration files.
const declarationFile = sourceFile.isDeclarationFile;
const moduleFile = ts.isExternalModule(sourceFile);
if (!declarationFile && moduleFile) {
for (let transform of this.transformers) {
const transformSubstitute = transform.start(sourceFile);
if (transformSubstitute) {
if (substitute) {
const previous: ValueTransform = substitute;
substitute = (value: MetadataValue, node: ts.Node) =>
transformSubstitute(previous(value, node), node);
} else {
substitute = transformSubstitute;
}
}
}
}
const result = this.collector.getMetadata(sourceFile, this.strict, substitute);
this.metadataCache.set(sourceFile.fileName, result);
return result;
}
}

View File

@ -13,16 +13,19 @@ import * as path from 'path';
import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {getAngularClassTransformerFactory} from './r3_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
/**
* Maximum number of files that are emitable via calling ts.Program.emit
* passing individual targetSourceFiles.
@ -43,7 +46,8 @@ const defaultEmitCallback: TsEmitCallback =
class AngularCompilerProgram implements Program {
private rootNames: string[];
private metadataCache: LowerMetadataCache;
private metadataCache: MetadataCache;
private loweringMetadataTransform: LowerMetadataTransform;
private oldProgramLibrarySummaries: Map<string, LibrarySummary>|undefined;
private oldProgramEmittedGeneratedFiles: Map<string, GeneratedFile>|undefined;
private oldProgramEmittedSourceFiles: Map<string, ts.SourceFile>|undefined;
@ -93,7 +97,14 @@ class AngularCompilerProgram implements Program {
this.host = bundleHost;
}
}
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
this.loweringMetadataTransform = new LowerMetadataTransform();
this.metadataCache = this.createMetadataCache([this.loweringMetadataTransform]);
}
private createMetadataCache(transformers: MetadataTransformer[]) {
return new MetadataCache(
new MetadataCollector({quotedNames: true}), !!this.options.strictMetadataEmit,
transformers);
}
getLibrarySummaries(): Map<string, LibrarySummary> {
@ -183,7 +194,7 @@ class AngularCompilerProgram implements Program {
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronsly');
throw new Error('Angular structure loaded both synchronously and asynchronously');
}
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
});
@ -231,7 +242,7 @@ class AngularCompilerProgram implements Program {
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
const tsCustomTansformers = this.calculateTransforms(
const tsCustomTransformers = this.calculateTransforms(
/* genFiles */ undefined, /* partialModules */ modules, customTransformers);
const emitResult = emitCallback({
@ -239,7 +250,7 @@ class AngularCompilerProgram implements Program {
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTansformers
customTransformers: tsCustomTransformers
});
return emitResult;
@ -293,7 +304,7 @@ class AngularCompilerProgram implements Program {
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
};
const tsCustomTansformers = this.calculateTransforms(
const tsCustomTransformers = this.calculateTransforms(
genFileByFileName, /* partialModules */ undefined, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
@ -330,7 +341,7 @@ class AngularCompilerProgram implements Program {
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTansformers,
customTransformers: tsCustomTransformers,
targetSourceFile: this.tsProgram.getSourceFile(fileName),
})));
emittedUserTsCount = sourceFilesToEmit.length;
@ -340,7 +351,7 @@ class AngularCompilerProgram implements Program {
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTansformers
customTransformers: tsCustomTransformers
});
emittedUserTsCount = this.tsProgram.getSourceFiles().length - genTsFiles.length;
}
@ -454,13 +465,19 @@ class AngularCompilerProgram implements Program {
customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
if (!this.options.disableExpressionLowering) {
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
beforeTs.push(
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
}
if (genFiles) {
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
}
if (partialModules) {
beforeTs.push(getAngularClassTransformerFactory(partialModules));
// If we have partial modules, the cached metadata might be incorrect as it doesn't reflect
// the partial module transforms.
this.metadataCache = this.createMetadataCache(
[this.loweringMetadataTransform, new PartialModuleMetadataTransformer(partialModules)]);
}
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
@ -505,7 +522,7 @@ class AngularCompilerProgram implements Program {
sourceFiles: string[],
} {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
throw new Error(`Internal Error: already initialized!`);
}
// Note: This is important to not produce a memory leak!
const oldTsProgram = this.oldTsProgram;
@ -522,7 +539,7 @@ class AngularCompilerProgram implements Program {
if (this.options.generateCodeForLibraries !== false) {
// if we should generateCodeForLibraries, never include
// generated files in the program as otherwise we will
// ovewrite them and typescript will report the error
// overwrite them and typescript will report the error
// TS5055: Cannot write file ... because it would overwrite input file.
rootNames = rootNames.filter(fn => !GENERATED_FILES.test(fn));
}
@ -551,7 +568,7 @@ class AngularCompilerProgram implements Program {
if (sf.fileName.endsWith('.ngfactory.ts')) {
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);
if (generate) {
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a basefileName
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a baseFileName
// for .ngfactory.ts files.
const genFile = this.compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
if (genFile) {
@ -674,7 +691,7 @@ class AngularCompilerProgram implements Program {
this.emittedLibrarySummaries.push({fileName: genFile.genFileUrl, text: outData});
if (!this.options.declaration) {
// If we don't emit declarations, still record an empty .ngfactory.d.ts file,
// as we might need it lateron for resolving module names from summaries.
// as we might need it later on for resolving module names from summaries.
const ngFactoryDts =
genFile.genFileUrl.substring(0, genFile.genFileUrl.length - 15) + '.ngfactory.d.ts';
this.emittedLibrarySummaries.push({fileName: ngFactoryDts, text: ''});
@ -688,7 +705,7 @@ class AngularCompilerProgram implements Program {
}
}
// Filter out generated files for which we didn't generate code.
// This can happen as the stub caclulation is not completely exact.
// This can happen as the stub calculation is not completely exact.
// Note: sourceFile refers to the .ngfactory.ts / .ngsummary.ts file
// node_emitter_transform already set the file contents to be empty,
// so this code only needs to skip the file if !allowEmptyCodegenFiles.
@ -855,7 +872,7 @@ export function i18nSerialize(
}
function getPathNormalizer(basePath?: string) {
// normalize sourcepaths by removing the base path and always using "/" as a separator
// normalize source paths by removing the base path and always using "/" as a separator
return (sourcePath: string) => {
sourcePath = basePath ? path.relative(basePath, sourcePath) : sourcePath;
return sourcePath.split(path.sep).join('/');

View File

@ -0,0 +1,55 @@
/**
* @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 {ClassStmt, PartialModule, Statement, StmtModifier} from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataCollector, MetadataValue, ModuleMetadata, isClassMetadata} from '../metadata/index';
import {MetadataTransformer, ValueTransform} from './metadata_cache';
export class PartialModuleMetadataTransformer implements MetadataTransformer {
private moduleMap: Map<string, PartialModule>;
constructor(modules: PartialModule[]) {
this.moduleMap = new Map(modules.map<[string, PartialModule]>(m => [m.fileName, m]));
}
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
const partialModule = this.moduleMap.get(sourceFile.fileName);
if (partialModule) {
const classMap = new Map<string, ClassStmt>(
partialModule.statements.filter(isClassStmt).map<[string, ClassStmt]>(s => [s.name, s]));
if (classMap.size > 0) {
return (value: MetadataValue, node: ts.Node): MetadataValue => {
// For class metadata that is going to be transformed to have a static method ensure the
// metadata contains a static declaration the new static method.
if (isClassMetadata(value) && node.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = node as ts.ClassDeclaration;
if (classDeclaration.name) {
const partialClass = classMap.get(classDeclaration.name.text);
if (partialClass) {
for (const field of partialClass.fields) {
if (field.name && field.modifiers &&
field.modifiers.some(modifier => modifier === StmtModifier.Static)) {
value.statics = {...(value.statics || {}), [field.name]: {}};
}
}
}
}
}
return value;
};
}
}
}
}
function isClassStmt(v: Statement): v is ClassStmt {
return v instanceof ClassStmt;
}

View File

@ -8,8 +8,9 @@
import * as ts from 'typescript';
import {ModuleMetadata} from '../../src/metadata/index';
import {LowerMetadataCache, LoweringRequest, RequestLocationMap, getExpressionLoweringTransformFactory} from '../../src/transformers/lower_expressions';
import {MetadataCollector, ModuleMetadata} from '../../src/metadata/index';
import {LowerMetadataTransform, LoweringRequest, RequestLocationMap, getExpressionLoweringTransformFactory} from '../../src/transformers/lower_expressions';
import {MetadataCache} from '../../src/transformers/metadata_cache';
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
describe('Expression lowering', () => {
@ -110,7 +111,8 @@ describe('Expression lowering', () => {
});
it('should throw a validation exception for invalid files', () => {
const cache = new LowerMetadataCache({}, /* strict */ true);
const cache = new MetadataCache(
new MetadataCollector({}), /* strict */ true, [new LowerMetadataTransform()]);
const sourceFile = ts.createSourceFile(
'foo.ts', `
import {Injectable} from '@angular/core';
@ -126,7 +128,8 @@ describe('Expression lowering', () => {
});
it('should not report validation errors on a .d.ts file', () => {
const cache = new LowerMetadataCache({}, /* strict */ true);
const cache = new MetadataCache(
new MetadataCollector({}), /* strict */ true, [new LowerMetadataTransform()]);
const dtsFile = ts.createSourceFile(
'foo.d.ts', `
import {Injectable} from '@angular/core';
@ -241,11 +244,12 @@ function normalizeResult(result: string): string {
function collect(annotatedSource: string) {
const {annotations, unannotatedSource} = getAnnotations(annotatedSource);
const cache = new LowerMetadataCache({});
const transformer = new LowerMetadataTransform();
const cache = new MetadataCache(new MetadataCollector({}), false, [transformer]);
const sourceFile = ts.createSourceFile(
'someName.ts', unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true);
return {
metadata: cache.getMetadata(sourceFile),
requests: cache.getRequests(sourceFile), annotations
requests: transformer.getRequests(sourceFile), annotations
};
}

View File

@ -0,0 +1,58 @@
/**
* @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 {ClassField, ClassMethod, ClassStmt, PartialModule, Statement, StmtModifier} from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataCollector, isClassMetadata} from '../../src/metadata/index';
import {MetadataCache} from '../../src/transformers/metadata_cache';
import {PartialModuleMetadataTransformer} from '../../src/transformers/r3_metadata_transform';
describe('r3_transform_spec', () => {
it('should add a static method to collected metadata', () => {
const fileName = '/some/directory/someFileName.ts';
const className = 'SomeClass';
const newFieldName = 'newStaticField';
const source = `
export class ${className} {
myMethod(): void {}
}
`;
const sourceFile =
ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
const partialModule: PartialModule = {
fileName,
statements: [new ClassStmt(
className, /* parent */ null, /* fields */[new ClassField(
/* name */ newFieldName, /* type */ null, /* modifiers */[StmtModifier.Static])],
/* getters */[],
/* constructorMethod */ new ClassMethod(/* name */ null, /* params */[], /* body */[]),
/* methods */[])]
};
const cache = new MetadataCache(
new MetadataCollector(), /* strict */ true,
[new PartialModuleMetadataTransformer([partialModule])]);
const metadata = cache.getMetadata(sourceFile);
expect(metadata).toBeDefined('Expected metadata from test source file');
if (metadata) {
const classData = metadata.metadata[className];
expect(classData && isClassMetadata(classData))
.toBeDefined(`Expected metadata to contain data for "${className}"`);
if (classData && isClassMetadata(classData)) {
const statics = classData.statics;
expect(statics).toBeDefined(`Expected "${className}" metadata to contain statics`);
if (statics) {
expect(statics[newFieldName]).toEqual({}, 'Expected new field to recorded as a function');
}
}
}
});
});

View File

@ -67,7 +67,7 @@ export * from './ml_parser/html_tags';
export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {NgModuleCompiler} from './ng_module_compiler';
export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast';
export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export * from './output/ts_emitter';
export * from './parse_util';