fix(compiler-cli): produce metadata for .d.ts files without metadata (#13526)

Fixes #13307
Fixes #13473
Fixes #13521
This commit is contained in:
Chuck Jazdzewski 2016-12-16 15:33:47 -08:00 committed by GitHub
parent e78508507d
commit 0e3981afc1
4 changed files with 123 additions and 62 deletions

View File

@ -181,6 +181,12 @@ export class CompilerHost implements AotCompilerHost {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.context.fileExists(metadataPath)) {
return this.readMetadata(metadataPath, filePath);
} else {
// If there is a .d.ts file but no metadata file we need to produce a
// v3 metadata from the .d.ts file as v3 includes the exports we need
// to resolve symbols.
return [this.upgradeVersion1Metadata(
{'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)];
}
} else {
const sf = this.getSourceFile(filePath);
@ -196,35 +202,13 @@ export class CompilerHost implements AotCompilerHost {
}
try {
const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath));
const metadatas = metadataOrMetadatas ?
const metadatas: ModuleMetadata[] = metadataOrMetadatas ?
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
[];
const v1Metadata = metadatas.find((m: any) => m['version'] === 1);
let v3Metadata = metadatas.find((m: any) => m['version'] === 3);
const v1Metadata = metadatas.find(m => m.version === 1);
let v3Metadata = metadatas.find(m => m.version === 3);
if (!v3Metadata && v1Metadata) {
// patch up v1 to v3 by merging the metadata with metadata collected from the d.ts file
// as the only difference between the versions is whether all exports are contained in
// the metadata and the `extends` clause.
v3Metadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}};
if (v1Metadata.exports) {
v3Metadata.exports = v1Metadata.exports;
}
for (let prop in v1Metadata.metadata) {
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) {
for (let prop in exports.metadata) {
if (!v3Metadata.metadata[prop]) {
v3Metadata.metadata[prop] = exports.metadata[prop];
}
}
if (exports.exports) {
v3Metadata.exports = exports.exports;
}
}
metadatas.push(v3Metadata);
metadatas.push(this.upgradeVersion1Metadata(v1Metadata, dtsFilePath));
}
this.resolverCache.set(filePath, metadatas);
return metadatas;
@ -234,6 +218,32 @@ export class CompilerHost implements AotCompilerHost {
}
}
private upgradeVersion1Metadata(v1Metadata: ModuleMetadata, dtsFilePath: string): ModuleMetadata {
// patch up v1 to v3 by merging the metadata with metadata collected from the d.ts file
// as the only difference between the versions is whether all exports are contained in
// the metadata and the `extends` clause.
let v3Metadata: ModuleMetadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}};
if (v1Metadata.exports) {
v3Metadata.exports = v1Metadata.exports;
}
for (let prop in v1Metadata.metadata) {
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) {
for (let prop in exports.metadata) {
if (!v3Metadata.metadata[prop]) {
v3Metadata.metadata[prop] = exports.metadata[prop];
}
}
if (exports.exports) {
v3Metadata.exports = exports.exports;
}
}
return v3Metadata;
}
loadResource(filePath: string): Promise<string> { return this.context.readResource(filePath); }
loadSummary(filePath: string): string|null {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ModuleMetadata} from '@angular/tsc-wrapped';
import * as ts from 'typescript';
import {CompilerHost} from '../src/compiler_host';
@ -150,12 +151,14 @@ describe('CompilerHost', () => {
it('should be able to read a metadata file', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/core.d.ts')).toEqual([
{__symbolic: 'module', version: 2, metadata: {foo: {__symbolic: 'class'}}}
{__symbolic: 'module', version: 3, metadata: {foo: {__symbolic: 'class'}}}
]);
});
it('should be able to read metadata from an otherwise unused .d.ts file ', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/unused.d.ts')).toBeUndefined();
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/unused.d.ts')).toEqual([
dummyMetadata
]);
});
it('should be able to read empty metadata ', () => {
@ -181,10 +184,21 @@ describe('CompilerHost', () => {
}
]);
});
it('should upgrade a missing metadata file into v3', () => {
expect(hostNestedGenDir.getMetadataFor('metadata_versions/v1_empty.d.ts')).toEqual([
{__symbolic: 'module', version: 3, metadata: {}, exports: [{from: './lib/utils'}]}
]);
});
});
const dummyModule = 'export let foo: any[];';
const dummyMetadata: ModuleMetadata = {
__symbolic: 'module',
version: 3,
metadata:
{foo: {__symbolic: 'error', message: 'Variable not initialized', line: 0, character: 11}}
};
const FILES: Entry = {
'tmp': {
'src': {
@ -204,7 +218,7 @@ const FILES: Entry = {
'@angular': {
'core.d.ts': dummyModule,
'core.metadata.json':
`{"__symbolic":"module", "version": 2, "metadata": {"foo": {"__symbolic": "class"}}}`,
`{"__symbolic":"module", "version": 3, "metadata": {"foo": {"__symbolic": "class"}}}`,
'router': {'index.d.ts': dummyModule, 'src': {'providers.d.ts': dummyModule}},
'unused.d.ts': dummyModule,
'empty.d.ts': 'export declare var a: string;',
@ -225,6 +239,9 @@ const FILES: Entry = {
`,
'v1.metadata.json':
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
'v1_empty.d.ts': `
export * from './lib/utils';
`
}
}
}

View File

@ -224,6 +224,34 @@ export class MetadataCollector {
return recordEntry(result, classDeclaration);
}
// Collect all exported symbols from an exports clause.
const exportMap = new Map<string, string>();
ts.forEachChild(sourceFile, node => {
switch (node.kind) {
case ts.SyntaxKind.ExportDeclaration:
const exportDeclaration = <ts.ExportDeclaration>node;
const {moduleSpecifier, exportClause} = exportDeclaration;
if (!moduleSpecifier) {
exportClause.elements.forEach(spec => {
const exportedAs = spec.name.text;
const name = (spec.propertyName || spec.name).text;
exportMap.set(name, exportedAs);
});
}
}
});
const isExportedIdentifier = (identifier: ts.Identifier) => exportMap.has(identifier.text);
const isExported = (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) =>
(node.flags & ts.NodeFlags.Export) || isExportedIdentifier(node.name);
const exportedIdentifierName = (identifier: ts.Identifier) =>
exportMap.get(identifier.text) || identifier.text;
const exportedName =
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) =>
exportedIdentifierName(node.name);
// Predeclare classes and functions
ts.forEachChild(sourceFile, node => {
switch (node.kind) {
@ -231,8 +259,9 @@ export class MetadataCollector {
const classDeclaration = <ts.ClassDeclaration>node;
if (classDeclaration.name) {
const className = classDeclaration.name.text;
if (node.flags & ts.NodeFlags.Export) {
locals.define(className, {__symbolic: 'reference', name: className});
if (isExported(classDeclaration)) {
locals.define(
className, {__symbolic: 'reference', name: exportedName(classDeclaration)});
} else {
locals.define(
className, errorSym('Reference to non-exported class', node, {className}));
@ -241,9 +270,9 @@ export class MetadataCollector {
break;
case ts.SyntaxKind.FunctionDeclaration:
if (!(node.flags & ts.NodeFlags.Export)) {
const functionDeclaration = <ts.FunctionDeclaration>node;
if (!isExported(functionDeclaration)) {
// Report references to this function as an error.
const functionDeclaration = <ts.FunctionDeclaration>node;
const nameNode = functionDeclaration.name;
if (nameNode && nameNode.text) {
locals.define(
@ -268,10 +297,14 @@ export class MetadataCollector {
if (exportClause) {
exportClause.elements.forEach(spec => {
const name = spec.name.text;
const propNode = spec.propertyName || spec.name;
const value: MetadataValue = evaluator.evaluateNode(propNode);
if (!metadata) metadata = {};
metadata[name] = recordEntry(value, node);
// If the symbol was not already exported, export a reference since it is a
// reference to an import
if (!metadata || !metadata[name]) {
const propNode = spec.propertyName || spec.name;
const value: MetadataValue = evaluator.evaluateNode(propNode);
if (!metadata) metadata = {};
metadata[name] = recordEntry(value, node);
}
});
}
}
@ -294,9 +327,9 @@ export class MetadataCollector {
const classDeclaration = <ts.ClassDeclaration>node;
if (classDeclaration.name) {
const className = classDeclaration.name.text;
if (node.flags & ts.NodeFlags.Export) {
if (isExported(classDeclaration)) {
if (!metadata) metadata = {};
metadata[className] = classMetadataOf(classDeclaration);
metadata[exportedName(classDeclaration)] = classMetadataOf(classDeclaration);
}
}
// Otherwise don't record metadata for the class.
@ -306,24 +339,20 @@ export class MetadataCollector {
// Record functions that return a single value. Record the parameter
// names substitution will be performed by the StaticReflector.
const functionDeclaration = <ts.FunctionDeclaration>node;
if (node.flags & ts.NodeFlags.Export) {
if (isExported(functionDeclaration)) {
if (!metadata) metadata = {};
const name = exportedName(functionDeclaration);
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
if (maybeFunc) {
metadata[maybeFunc.name] = recordEntry(maybeFunc.func, node);
} else if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
const nameNode = <ts.Identifier>functionDeclaration.name;
const functionName = nameNode.text;
metadata[functionName] = {__symbolic: 'function'};
}
metadata[name] =
maybeFunc ? recordEntry(maybeFunc.func, node) : {__symbolic: 'function'};
}
break;
case ts.SyntaxKind.EnumDeclaration:
if (node.flags & ts.NodeFlags.Export) {
const enumDeclaration = <ts.EnumDeclaration>node;
const enumDeclaration = <ts.EnumDeclaration>node;
if (isExported(enumDeclaration)) {
const enumValueHolder: {[name: string]: MetadataValue} = {};
const enumName = enumDeclaration.name.text;
const enumName = exportedName(enumDeclaration);
let nextDefaultValue: MetadataValue = 0;
let writtenMembers = 0;
for (const member of enumDeclaration.members) {
@ -376,9 +405,10 @@ export class MetadataCollector {
}
let exported = false;
if (variableStatement.flags & ts.NodeFlags.Export ||
variableDeclaration.flags & ts.NodeFlags.Export) {
variableDeclaration.flags & ts.NodeFlags.Export ||
isExportedIdentifier(nameNode)) {
if (!metadata) metadata = {};
metadata[nameNode.text] = recordEntry(varValue, node);
metadata[exportedIdentifierName(nameNode)] = recordEntry(varValue, node);
exported = true;
}
if (isPrimitive(varValue)) {

View File

@ -40,6 +40,7 @@ describe('Collector', () => {
'private-enum.ts',
're-exports.ts',
're-exports-2.ts',
'export-as.d.ts',
'static-field-reference.ts',
'static-method.ts',
'static-method-call.ts',
@ -528,20 +529,19 @@ describe('Collector', () => {
]);
});
it('should be able to collect a export as symbol', () => {
const source = program.getSourceFile('export-as.d.ts');
const metadata = collector.getMetadata(source);
expect(metadata.metadata).toEqual({SomeFunction: {__symbolic: 'function'}});
});
it('should be able to collect exports with no module specifier', () => {
const source = program.getSourceFile('/re-exports-2.ts');
const metadata = collector.getMetadata(source);
expect(metadata.metadata).toEqual({
MyClass: Object({__symbolic: 'class'}),
OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'},
MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'},
// TODO(vicb): support exported symbols - https://github.com/angular/angular/issues/13473
MyClass: {
__symbolic: 'error',
message: 'Reference to non-exported class',
line: 3,
character: 4,
context: {className: 'MyClass'}
},
MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'}
});
});
@ -1007,6 +1007,10 @@ const FILES: Directory = {
class MyClass {}
export {OtherModule, MyModule as MyOtherModule, MyClass};
`,
'export-as.d.ts': `
declare function someFunction(): void;
export { someFunction as SomeFunction };
`,
'local-symbol-ref.ts': `
import {Component, Validators} from 'angular2/core';