Skip to content

Commit

Permalink
fix(CompilerCli): assert that all pipes and directives are declared b…
Browse files Browse the repository at this point in the history
…y a module
  • Loading branch information
vicb committed Oct 26, 2016
1 parent 02f1222 commit 7221632
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 131 deletions.
103 changes: 44 additions & 59 deletions modules/@angular/compiler-cli/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Intended to be used in a build step.
*/
import * as compiler from '@angular/compiler';
import {Directive, NgModule, ViewEncapsulation} from '@angular/core';
import {ViewEncapsulation} from '@angular/core';
import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped';
import * as path from 'path';
import * as ts from 'typescript';
Expand All @@ -35,65 +35,11 @@ const PREAMBLE = `/**
`;

export class CodeGeneratorModuleCollector {
constructor(
private staticReflector: StaticReflector, private reflectorHost: StaticReflectorHost,
private program: ts.Program, private options: AngularCompilerOptions) {}

getModuleSymbols(): StaticSymbol[] {
// Compare with false since the default should be true
const skipFileNames =
this.options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES : GENERATED_FILES;

const ngModules: StaticSymbol[] = [];

this.program.getSourceFiles()
.filter(sourceFile => !skipFileNames.test(sourceFile.fileName))
.forEach(sourceFile => {
const absSrcPath = this.reflectorHost.getCanonicalFileName(sourceFile.fileName);

const moduleMetadata = this.staticReflector.getModuleMetadata(absSrcPath);
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSrcPath}`);
return;
}

const metadata = moduleMetadata['metadata'];

if (!metadata) {
return;
}

for (const symbol of Object.keys(metadata)) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
const staticType = this.reflectorHost.findDeclaration(absSrcPath, symbol, absSrcPath);
const annotations = this.staticReflector.annotations(staticType);
annotations.some((annotation) => {
if (annotation instanceof NgModule) {
ngModules.push(staticType);
return true;
}
});
}
});

return ngModules;
}
}

export class CodeGenerator {
private moduleCollector: CodeGeneratorModuleCollector;

constructor(
private options: AngularCompilerOptions, private program: ts.Program,
public host: ts.CompilerHost, private staticReflector: StaticReflector,
private compiler: compiler.OfflineCompiler, private reflectorHost: StaticReflectorHost) {
this.moduleCollector =
new CodeGeneratorModuleCollector(staticReflector, reflectorHost, program, options);
}
private compiler: compiler.OfflineCompiler, private reflectorHost: StaticReflectorHost) {}

// Write codegen in a directory structure matching the sources.
private calculateEmitPath(filePath: string): string {
Expand All @@ -118,10 +64,11 @@ export class CodeGenerator {
return path.join(this.options.genDir, relativePath);
}

codegen(): Promise<any> {
const ngModules = this.moduleCollector.getModuleSymbols();
codegen(options: {transitiveModules: boolean}): Promise<any> {
const staticSymbols =
extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);

return this.compiler.compileModules(ngModules).then(generatedModules => {
return this.compiler.compileModules(staticSymbols, options).then(generatedModules => {
generatedModules.forEach(generatedModule => {
const sourceFile = this.program.getSourceFile(generatedModule.fileUrl);
const emitPath = this.calculateEmitPath(generatedModule.moduleUrl);
Expand Down Expand Up @@ -194,3 +141,41 @@ export class CodeGenerator {
options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost);
}
}

export function extractProgramSymbols(
program: ts.Program, staticReflector: StaticReflector, reflectorHost: StaticReflectorHost,
options: AngularCompilerOptions): StaticSymbol[] {
// Compare with false since the default should be true
const skipFileNames =
options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES : GENERATED_FILES;

const staticSymbols: StaticSymbol[] = [];

program.getSourceFiles()
.filter(sourceFile => !skipFileNames.test(sourceFile.fileName))
.forEach(sourceFile => {
const absSrcPath = reflectorHost.getCanonicalFileName(sourceFile.fileName);

const moduleMetadata = staticReflector.getModuleMetadata(absSrcPath);
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSrcPath}`);
return;
}

const metadata = moduleMetadata['metadata'];

if (!metadata) {
return;
}

for (const symbol of Object.keys(metadata)) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
staticSymbols.push(reflectorHost.findDeclaration(absSrcPath, symbol, absSrcPath));
}
});

return staticSymbols;
}
54 changes: 8 additions & 46 deletions modules/@angular/compiler-cli/src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,20 @@

/**
* Extract i18n messages from source code
*
* TODO(vicb): factorize code with the CodeGenerator
*/
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';

import * as compiler from '@angular/compiler';
import {Component, NgModule, ViewEncapsulation} from '@angular/core';
import {ViewEncapsulation} from '@angular/core';
import * as tsc from '@angular/tsc-wrapped';
import * as ts from 'typescript';

import {ReflectorHost, ReflectorHostContext} from './reflector_host';
import {extractProgramSymbols} from './codegen';
import {ReflectorHost} from './reflector_host';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector, StaticSymbol} from './static_reflector';

const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;

export class Extractor {
constructor(
private options: tsc.AngularCompilerOptions, private program: ts.Program,
Expand All @@ -34,48 +31,13 @@ export class Extractor {
private metadataResolver: compiler.CompileMetadataResolver,
private directiveNormalizer: compiler.DirectiveNormalizer) {}

private readModuleSymbols(absSourcePath: string): StaticSymbol[] {
const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath);
const modSymbols: StaticSymbol[] = [];
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`);
return modSymbols;
}

const metadata = moduleMetadata['metadata'];
const symbols = metadata && Object.keys(metadata);
if (!symbols || !symbols.length) {
return modSymbols;
}

for (const symbol of symbols) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}

const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
const annotations = this.staticReflector.annotations(staticType);

annotations.some(a => {
if (a instanceof NgModule) {
modSymbols.push(staticType);
return true;
}
});
}

return modSymbols;
}

extract(): Promise<compiler.MessageBundle> {
const filePaths =
this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f));
const ngModules: StaticSymbol[] = [];

filePaths.forEach((filePath) => ngModules.push(...this.readModuleSymbols(filePath)));
const programSymbols: StaticSymbol[] =
extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);

const files = compiler.analyzeNgModules(ngModules, this.metadataResolver).files;
const files =
compiler.analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver)
.files;
const errors: compiler.ParseError[] = [];
const filePromises: Promise<any>[] = [];

Expand Down
4 changes: 3 additions & 1 deletion modules/@angular/compiler-cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {CodeGenerator} from './codegen';
function codegen(
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program,
host: ts.CompilerHost) {
return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen();
return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen({
transitiveModules: true
});
}

// CLI entry point
Expand Down
103 changes: 78 additions & 25 deletions modules/@angular/compiler/src/offline_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,56 +29,81 @@ export class SourceModule {

// Returns all the source files and a mapping from modules to directives
export function analyzeNgModules(
ngModules: StaticSymbol[], metadataResolver: CompileMetadataResolver): {
ngModuleByDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): {
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}>
} {
const {
ngModules: programNgModules,
pipesAndDirectives: programPipesOrDirectives,
} = _extractModulesAndPipesOrDirectives(programStaticSymbols, metadataResolver);

const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();

// For every input modules add the list of transitively included modules
ngModules.forEach(ngModule => {
const modMeta = metadataResolver.getNgModuleMetadata(ngModule);
modMeta.transitiveModule.modules.forEach(
modMeta => { moduleMetasByRef.set(modMeta.type.reference, modMeta); });
programNgModules.forEach(modMeta => {
if (options.transitiveModules) {
// For every input modules add the list of transitively included modules
modMeta.transitiveModule.modules.forEach(
modMeta => { moduleMetasByRef.set(modMeta.type.reference, modMeta); });
} else {
moduleMetasByRef.set(modMeta.type.reference, modMeta);
}
});

const ngModuleMetas = MapWrapper.values(moduleMetasByRef);
const ngModuleByDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModulesByFile = new Map<string, StaticSymbol[]>();
const ngDirectivesByFile = new Map<string, StaticSymbol[]>();
const srcFileUrls = new Set<string>();
const filePaths = new Set<string>();

// Looping over all modules to construct:
// - a map from files to modules `ngModulesByFile`,
// - a map from files to directives `ngDirectivesByFile`,
// - a map from modules to directives `ngModuleByDirective`.
// - a map from file to modules `ngModulesByFile`,
// - a map from file to directives `ngDirectivesByFile`,
// - a map from directive/pipe to module `ngModuleByPipeOrDirective`.
ngModuleMetas.forEach((ngModuleMeta) => {
const srcFileUrl = ngModuleMeta.type.reference.filePath;
srcFileUrls.add(srcFileUrl);
filePaths.add(srcFileUrl);
ngModulesByFile.set(
srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference));

ngModuleMeta.declaredDirectives.forEach((dirMeta: CompileDirectiveMetadata) => {
const fileUrl = dirMeta.type.reference.filePath;
srcFileUrls.add(fileUrl);
filePaths.add(fileUrl);
ngDirectivesByFile.set(
fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirMeta.type.reference));
ngModuleByDirective.set(dirMeta.type.reference, ngModuleMeta);
ngModuleByPipeOrDirective.set(dirMeta.type.reference, ngModuleMeta);
});

ngModuleMeta.declaredPipes.forEach((pipeMeta: CompilePipeMetadata) => {
const fileUrl = pipeMeta.type.reference.filePath;
filePaths.add(fileUrl);
ngModuleByPipeOrDirective.set(pipeMeta.type.reference, ngModuleMeta);
});
});

// Throw an error if any of the program pipe or directives is not declared by a module
const symbolsMissingModule =
programPipesOrDirectives.filter(s => !ngModuleByPipeOrDirective.has(s));

if (symbolsMissingModule.length) {
const messages = symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}

const files: {srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}[] = [];

srcFileUrls.forEach((srcUrl) => {
filePaths.forEach((srcUrl) => {
const directives = ngDirectivesByFile.get(srcUrl) || [];
const ngModules = ngModulesByFile.get(srcUrl) || [];
files.push({srcUrl, directives, ngModules});
});

return {
// map from modules to declared directives
ngModuleByDirective,
// list of modules and directives for every source file
// map directive/pipe to module
ngModuleByPipeOrDirective,
// list modules and directives for every source file
files,
};
}
Expand All @@ -100,19 +125,21 @@ export class OfflineCompiler {
this._metadataResolver.clearCache();
}

compileModules(ngModules: StaticSymbol[]): Promise<SourceModule[]> {
const {ngModuleByDirective, files} = analyzeNgModules(ngModules, this._metadataResolver);
compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}):
Promise<SourceModule[]> {
const {ngModuleByPipeOrDirective, files} =
analyzeNgModules(staticSymbols, options, this._metadataResolver);

const sourceModules = files.map(
file => this._compileSrcFile(
file.srcUrl, ngModuleByDirective, file.directives, file.ngModules));
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules));

return Promise.all(sourceModules)
.then((modules: SourceModule[][]) => ListWrapper.flatten(modules));
}

private _compileSrcFile(
srcFileUrl: string, ngModuleByDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], ngModules: StaticSymbol[]): Promise<SourceModule[]> {
const fileSuffix = _splitTypescriptSuffix(srcFileUrl)[1];
const statements: o.Statement[] = [];
Expand All @@ -134,9 +161,10 @@ export class OfflineCompiler {
if (!compMeta.isComponent) {
return Promise.resolve(null);
}
const ngModule = ngModuleByDirective.get(dirType);
const ngModule = ngModuleByPipeOrDirective.get(dirType);
if (!ngModule) {
throw new Error(`Cannot determine the module for component ${compMeta.type.name}!`);
throw new Error(
`Internal Error: cannot determine the module for component ${compMeta.type.name}!`);
}

return Promise
Expand Down Expand Up @@ -330,3 +358,28 @@ function _splitTypescriptSuffix(path: string): string[] {

return [path, ''];
}

// Group the symbols by types:
// - NgModules,
// - Pipes and Directives.
function _extractModulesAndPipesOrDirectives(
programStaticSymbols: StaticSymbol[], metadataResolver: CompileMetadataResolver) {
const ngModules: CompileNgModuleMetadata[] = [];
const pipesAndDirectives: StaticSymbol[] = [];

programStaticSymbols.forEach(staticSymbol => {
const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false);
const directive = metadataResolver.getDirectiveMetadata(staticSymbol, false);
const pipe = metadataResolver.getPipeMetadata(<any>staticSymbol, false);

if (ngModule) {
ngModules.push(ngModule);
} else if (directive) {
pipesAndDirectives.push(staticSymbol);
} else if (pipe) {
pipesAndDirectives.push(staticSymbol);
}
});

return {ngModules, pipesAndDirectives};
}

0 comments on commit 7221632

Please sign in to comment.