Skip to content

Commit

Permalink
fix(compiler-cli): produce metadata for .d.ts files without metadata (a…
Browse files Browse the repository at this point in the history
  • Loading branch information
chuckjaz authored Dec 16, 2016
1 parent e785085 commit 0e3981a
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 62 deletions.
62 changes: 36 additions & 26 deletions modules/@angular/compiler-cli/src/compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
25 changes: 21 additions & 4 deletions modules/@angular/compiler-cli/test/aot_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 ', () => {
Expand All @@ -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': {
Expand All @@ -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;',
Expand All @@ -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';
`
}
}
}
Expand Down
76 changes: 53 additions & 23 deletions tools/@angular/tsc-wrapped/src/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,44 @@ 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) {
case ts.SyntaxKind.ClassDeclaration:
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}));
Expand All @@ -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(
Expand All @@ -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);
}
});
}
}
Expand All @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand Down
22 changes: 13 additions & 9 deletions tools/@angular/tsc-wrapped/test/collector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'}
});
});

Expand Down Expand Up @@ -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';
Expand Down

0 comments on commit 0e3981a

Please sign in to comment.