From 4695c69cf115b5713cfa6237d3f6e93b9dd90d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 13 Sep 2017 16:55:42 -0700 Subject: [PATCH] refactor(compiler): remove all source-level traces to tsc-wrapped (#18966) - temporarily keeps the old sources under packages/tsc-wrapped until the build scripts are changed to use compiler-cli everywhere. - removes the compiler options `disableTransformerPipeline` that was introduced in a previous beta of Angular 5, i.e. the transformer based compiler is now always enabled. PR Close #18966 --- packages/bazel/package.json | 5 +- packages/bazel/src/ngc-wrapped/index.ts | 2 + packages/compiler-cli/index.ts | 13 +- .../integrationtest/test/test_summaries.ts | 4 +- packages/compiler-cli/src/codegen.ts | 10 +- packages/compiler-cli/src/compiler_host.ts | 10 +- packages/compiler-cli/src/extract_i18n.ts | 11 +- packages/compiler-cli/src/extractor.ts | 6 +- .../compiler-cli/src/language_services.ts | 3 +- packages/compiler-cli/src/main.ts | 51 +- .../src/metadata/bundle_index_host.ts | 89 + packages/compiler-cli/src/metadata/bundler.ts | 628 +++++++ .../compiler-cli/src/metadata/collector.ts | 774 +++++++++ .../compiler-cli/src/metadata/evaluator.ts | 680 ++++++++ packages/compiler-cli/src/metadata/index.ts | 11 + .../compiler-cli/src/metadata/index_writer.ts | 58 + packages/compiler-cli/src/metadata/schema.ts | 284 ++++ packages/compiler-cli/src/metadata/symbols.ts | 130 ++ packages/compiler-cli/src/ngtools_api.ts | 8 +- .../src/path_mapped_compiler_host.ts | 5 +- packages/compiler-cli/src/perform_compile.ts | 1 - .../src/transformers/lower_expressions.ts | 2 +- .../compiler-cli/src/transformers/program.ts | 2 +- packages/compiler-cli/test/aot_host_spec.ts | 2 +- .../expression_diagnostics_spec.ts | 5 +- .../test/diagnostics/symbol_query_spec.ts | 5 +- .../compiler-cli/test/extract_i18n_spec.ts | 15 +- packages/compiler-cli/test/main_spec.ts | 313 ---- .../test/metadata/bundler_spec.ts | 274 +++ .../test/metadata/collector_spec.ts | 1484 +++++++++++++++++ .../test/metadata/evaluator_spec.ts | 373 +++++ .../test/metadata/index_writer_spec.ts | 25 + .../test/metadata/symbols_spec.ts | 133 ++ .../test/metadata/typescript.mocks.ts | 204 +++ packages/compiler-cli/test/ngc_spec.ts | 129 +- packages/compiler-cli/test/test_support.ts | 28 + .../transformers/lower_expressions_spec.ts | 2 +- packages/compiler/test/aot/compiler_spec.ts | 3 +- .../test/aot/static_reflector_spec.ts | 2 +- .../test/aot/static_symbol_resolver_spec.ts | 3 +- packages/compiler/test/aot/test_util.ts | 4 +- .../language-service/src/reflector_host.ts | 4 +- .../language-service/src/typescript_host.ts | 4 +- tools/build/linknodemodules.js | 67 - 44 files changed, 5350 insertions(+), 516 deletions(-) create mode 100644 packages/compiler-cli/src/metadata/bundle_index_host.ts create mode 100644 packages/compiler-cli/src/metadata/bundler.ts create mode 100644 packages/compiler-cli/src/metadata/collector.ts create mode 100644 packages/compiler-cli/src/metadata/evaluator.ts create mode 100644 packages/compiler-cli/src/metadata/index.ts create mode 100644 packages/compiler-cli/src/metadata/index_writer.ts create mode 100644 packages/compiler-cli/src/metadata/schema.ts create mode 100644 packages/compiler-cli/src/metadata/symbols.ts delete mode 100644 packages/compiler-cli/test/main_spec.ts create mode 100644 packages/compiler-cli/test/metadata/bundler_spec.ts create mode 100644 packages/compiler-cli/test/metadata/collector_spec.ts create mode 100644 packages/compiler-cli/test/metadata/evaluator_spec.ts create mode 100644 packages/compiler-cli/test/metadata/index_writer_spec.ts create mode 100644 packages/compiler-cli/test/metadata/symbols_spec.ts create mode 100644 packages/compiler-cli/test/metadata/typescript.mocks.ts create mode 100644 packages/compiler-cli/test/test_support.ts delete mode 100644 tools/build/linknodemodules.js diff --git a/packages/bazel/package.json b/packages/bazel/package.json index e770464f89..1360531fba 100644 --- a/packages/bazel/package.json +++ b/packages/bazel/package.json @@ -9,10 +9,11 @@ "typescript": "^2.4.2" }, "dependencies": { - "@bazel/typescript": "0.1.x" + "@bazel/typescript": "0.1.x", + "@types/node": "6.0.84" }, "repository": { "type": "git", "url": "https://github.com/angular/angular.git" } -} \ No newline at end of file +} diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index 9fb767f6d6..6d45e0c904 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -5,6 +5,8 @@ * 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 */ +// TODO(tbosch): figure out why we need this as it breaks node code within ngc-wrapped +/// import * as ng from '@angular/compiler-cli'; import {BazelOptions, CachedFileLoader, CompilerHost, FileCache, FileLoader, UncachedFileLoader, constructManifest, debug, parseTsconfig, runAsWorker, runWorkerLoop} from '@bazel/typescript'; import * as fs from 'fs'; diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index 94cc324c6c..b94cacfc5d 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -8,19 +8,20 @@ export {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler'; export {CodeGenerator} from './src/codegen'; export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './src/compiler_host'; -export {Extractor} from './src/extractor'; -export * from '@angular/tsc-wrapped'; -export {VERSION} from './src/version'; - -export {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics, getExpressionScope} from './src/diagnostics/expression_diagnostics'; +export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics'; export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type'; -export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols'; export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols'; +export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols'; +export {Extractor} from './src/extractor'; +export {VERSION} from './src/version'; export * from './src/transformers/api'; export * from './src/transformers/entry_points'; export * from './src/perform_compile'; +// TODO(tbosch): remove this once everyone is on transformers +export {CompilerOptions as AngularCompilerOptions} from './src/transformers/api'; + // TODO(hansl): moving to Angular 4 need to update this API. export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'; diff --git a/packages/compiler-cli/integrationtest/test/test_summaries.ts b/packages/compiler-cli/integrationtest/test/test_summaries.ts index cc6abeceda..e73b1cf2df 100644 --- a/packages/compiler-cli/integrationtest/test/test_summaries.ts +++ b/packages/compiler-cli/integrationtest/test/test_summaries.ts @@ -15,7 +15,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import * as assert from 'assert'; import {tsc} from '@angular/tsc-wrapped/src/tsc'; -import {AngularCompilerOptions, CodeGenerator, CompilerHostContext, NodeCompilerHostContext} from '@angular/compiler-cli'; +import {CompilerOptions, CodeGenerator, CompilerHostContext, NodeCompilerHostContext} from '@angular/compiler-cli'; /** * Main method. @@ -89,7 +89,7 @@ function main() { * Simple adaption of tsc-wrapped main to just run codegen with a CompilerHostContext */ function codegen( - config: {parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions}, + config: {parsed: ts.ParsedCommandLine, ngOptions: CompilerOptions}, hostContextFactory: (host: ts.CompilerHost) => CompilerHostContext) { const host = ts.createCompilerHost(config.parsed.options, true); diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index 3b7a7ca985..5b00a9b412 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -11,12 +11,12 @@ * Intended to be used in a build step. */ import * as compiler from '@angular/compiler'; -import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped'; import {readFileSync} from 'fs'; import * as ts from 'typescript'; import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host'; import {PathMappedCompilerHost} from './path_mapped_compiler_host'; +import {CompilerOptions} from './transformers/api'; const GENERATED_META_FILES = /\.json$/; @@ -36,11 +36,11 @@ export interface CodeGeneratorI18nOptions { missingTranslation: string|null; } +// TODO(tbosch): remove this once G3 uses the transformer compiler! export class CodeGenerator { constructor( - private options: AngularCompilerOptions, private program: ts.Program, - public host: ts.CompilerHost, private compiler: compiler.AotCompiler, - private ngCompilerHost: CompilerHost) {} + private options: CompilerOptions, private program: ts.Program, public host: ts.CompilerHost, + private compiler: compiler.AotCompiler, private ngCompilerHost: CompilerHost) {} codegen(): Promise { return this.compiler @@ -67,7 +67,7 @@ export class CodeGenerator { } static create( - options: AngularCompilerOptions, i18nOptions: CodeGeneratorI18nOptions, program: ts.Program, + options: CompilerOptions, i18nOptions: CodeGeneratorI18nOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost, compilerHostContext?: CompilerHostContext, ngCompilerHost?: CompilerHost): CodeGenerator { if (!ngCompilerHost) { diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 63b7d595d4..9c8f143e61 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -7,11 +7,13 @@ */ import {AotCompilerHost, StaticSymbol, UrlResolver, createOfflineCompileUrlResolver, syntaxError} from '@angular/compiler'; -import {AngularCompilerOptions, CollectorOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; +import {CollectorOptions, MetadataCollector, ModuleMetadata} from './metadata/index'; +import {CompilerOptions} from './transformers/api'; + const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const DTS = /\.d\.ts$/; const NODE_MODULES = '/node_modules/'; @@ -34,8 +36,7 @@ export abstract class BaseAotCompilerHost private flatModuleIndexRedirectNames = new Set(); constructor( - protected program: ts.Program, protected options: AngularCompilerOptions, - protected context: C, + protected program: ts.Program, protected options: CompilerOptions, protected context: C, protected metadataProvider: MetadataProvider = new MetadataCollector()) {} abstract moduleNameToFileName(m: string, containingFile: string): string|null; @@ -230,6 +231,7 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost { assumeFileExists(fileName: string): void; } +// TODO(tbosch): remove this once G3 uses the transformer compiler! export class CompilerHost extends BaseAotCompilerHost { protected basePath: string; private moduleFileNames = new Map(); @@ -239,7 +241,7 @@ export class CompilerHost extends BaseAotCompilerHost { private urlResolver: UrlResolver; constructor( - program: ts.Program, options: AngularCompilerOptions, context: CompilerHostContext, + program: ts.Program, options: CompilerOptions, context: CompilerHostContext, collectorOptions?: CollectorOptions, metadataProvider: MetadataProvider = new MetadataCollector(collectorOptions)) { super(program, options, context, metadataProvider); diff --git a/packages/compiler-cli/src/extract_i18n.ts b/packages/compiler-cli/src/extract_i18n.ts index 67867515e8..7cc9bb4359 100644 --- a/packages/compiler-cli/src/extract_i18n.ts +++ b/packages/compiler-cli/src/extract_i18n.ts @@ -15,11 +15,12 @@ import 'reflect-metadata'; import * as api from './transformers/api'; import {ParsedConfiguration} from './perform_compile'; -import {mainSync, readCommandLineAndConfiguration} from './main'; +import {main, readCommandLineAndConfiguration} from './main'; -export function main(args: string[], consoleError: (msg: string) => void = console.error): number { +export function mainXi18n( + args: string[], consoleError: (msg: string) => void = console.error): number { const config = readXi18nCommandLineAndConfiguration(args); - return mainSync(args, consoleError, config); + return main(args, consoleError, config); } function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration { @@ -41,5 +42,5 @@ function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfigurati // Entry point if (require.main === module) { const args = process.argv.slice(2); - process.exitCode = main(args); -} \ No newline at end of file + process.exitCode = mainXi18n(args); +} diff --git a/packages/compiler-cli/src/extractor.ts b/packages/compiler-cli/src/extractor.ts index e4b23e607a..4a55f3b07f 100644 --- a/packages/compiler-cli/src/extractor.ts +++ b/packages/compiler-cli/src/extractor.ts @@ -14,17 +14,17 @@ import 'reflect-metadata'; import * as compiler from '@angular/compiler'; -import * as tsc from '@angular/tsc-wrapped'; import * as path from 'path'; import * as ts from 'typescript'; import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host'; import {PathMappedCompilerHost} from './path_mapped_compiler_host'; +import {CompilerOptions} from './transformers/api'; import {i18nExtract, i18nGetExtension, i18nSerialize} from './transformers/program'; export class Extractor { constructor( - private options: tsc.AngularCompilerOptions, private ngExtractor: compiler.Extractor, + private options: CompilerOptions, private ngExtractor: compiler.Extractor, public host: ts.CompilerHost, private ngCompilerHost: CompilerHost, private program: ts.Program) {} @@ -47,7 +47,7 @@ export class Extractor { getExtension(formatName: string): string { return i18nGetExtension(formatName); } static create( - options: tsc.AngularCompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost, + options: CompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost, locale?: string|null, compilerHostContext?: CompilerHostContext, ngCompilerHost?: CompilerHost): Extractor { if (!ngCompilerHost) { diff --git a/packages/compiler-cli/src/language_services.ts b/packages/compiler-cli/src/language_services.ts index 34fd77fd23..a875c96a7f 100644 --- a/packages/compiler-cli/src/language_services.ts +++ b/packages/compiler-cli/src/language_services.ts @@ -14,11 +14,10 @@ Angular modules and Typescript as this will indirectly add a dependency to the language service. */ - -export {AngularCompilerOptions} from '@angular/tsc-wrapped'; export {CompilerHost, CompilerHostContext, MetadataProvider, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host'; export {TypeChecker} from './diagnostics/check_types'; export {DiagnosticTemplateInfo, ExpressionDiagnostic, getExpressionDiagnostics, getExpressionScope, getTemplateExpressionDiagnostics} from './diagnostics/expression_diagnostics'; export {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './diagnostics/expression_type'; export {BuiltinType, DeclarationKind, Definition, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './diagnostics/symbols'; export {getClassFromStaticSymbol, getClassMembers, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './diagnostics/typescript_symbols'; +export {CompilerOptions} from './transformers/api'; diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 50bc5cac69..318e754c08 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -11,7 +11,6 @@ import 'reflect-metadata'; import * as ts from 'typescript'; -import * as tsc from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as tsickle from 'tsickle'; @@ -21,29 +20,8 @@ import * as ngc from './transformers/entry_points'; import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile'; import {performWatchCompilation, createPerformWatchHost} from './perform_watch'; import {isSyntaxError} from '@angular/compiler'; -import {CodeGenerator} from './codegen'; -// TODO(tbosch): remove this old entrypoint once we drop `disableTransformerPipeline`. export function main( - args: string[], consoleError: (s: string) => void = console.error): Promise { - let {project, rootNames, options, errors: configErrors, watch} = - readNgcCommandLineAndConfiguration(args); - if (configErrors.length) { - return Promise.resolve(reportErrorsAndExit(options, configErrors, consoleError)); - } - if (watch) { - const result = watchMode(project, options, consoleError); - return Promise.resolve(reportErrorsAndExit({}, result.firstCompileResult, consoleError)); - } - if (options.disableTransformerPipeline) { - return disabledTransformerPipelineNgcMain(args, consoleError); - } - const {diagnostics: compileDiags} = - performCompilation({rootNames, options, emitCallback: createEmitCallback(options)}); - return Promise.resolve(reportErrorsAndExit(options, compileDiags, consoleError)); -} - -export function mainSync( args: string[], consoleError: (s: string) => void = console.error, config?: NgcParsedConfiguration): number { let {project, rootNames, options, errors: configErrors, watch, emitFlags} = @@ -160,35 +138,8 @@ export function watchMode( }, options, options => createEmitCallback(options))); } -function disabledTransformerPipelineNgcMain( - args: string[], consoleError: (s: string) => void = console.error): Promise { - const parsedArgs = require('minimist')(args); - const cliOptions = new tsc.NgcCliOptions(parsedArgs); - const project = parsedArgs.p || parsedArgs.project || '.'; - return tsc.main(project, cliOptions, disabledTransformerPipelineCodegen) - .then(() => 0) - .catch(e => { - if (e instanceof tsc.UserError || isSyntaxError(e)) { - consoleError(e.message); - } else { - consoleError(e.stack); - } - return Promise.resolve(1); - }); -} - -function disabledTransformerPipelineCodegen( - ngOptions: api.CompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program, - host: ts.CompilerHost) { - if (ngOptions.enableSummariesForJit === undefined) { - // default to false - ngOptions.enableSummariesForJit = false; - } - return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen(); -} - // CLI entry point if (require.main === module) { const args = process.argv.slice(2); - process.exitCode = mainSync(args); + process.exitCode = main(args); } diff --git a/packages/compiler-cli/src/metadata/bundle_index_host.ts b/packages/compiler-cli/src/metadata/bundle_index_host.ts new file mode 100644 index 0000000000..fb7af2ea1f --- /dev/null +++ b/packages/compiler-cli/src/metadata/bundle_index_host.ts @@ -0,0 +1,89 @@ +/** + * @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 fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import {CompilerOptions} from '../transformers/api'; + +import {CompilerHostAdapter, MetadataBundler} from './bundler'; +import {privateEntriesToIndex} from './index_writer'; + +const DTS = /\.d\.ts$/; +const JS_EXT = /(\.js|)$/; + +function createSyntheticIndexHost( + delegate: H, syntheticIndex: {name: string, content: string, metadata: string}): H { + const normalSyntheticIndexName = path.normalize(syntheticIndex.name); + const indexContent = syntheticIndex.content; + const indexMetadata = syntheticIndex.metadata; + + const newHost = Object.create(delegate); + newHost.fileExists = (fileName: string): boolean => { + return path.normalize(fileName) == normalSyntheticIndexName || delegate.fileExists(fileName); + }; + + newHost.readFile = (fileName: string) => { + return path.normalize(fileName) == normalSyntheticIndexName ? indexContent : + delegate.readFile(fileName); + }; + + newHost.getSourceFile = + (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => { + if (path.normalize(fileName) == normalSyntheticIndexName) { + return ts.createSourceFile(fileName, indexContent, languageVersion, true); + } + return delegate.getSourceFile(fileName, languageVersion, onError); + }; + + newHost.writeFile = + (fileName: string, data: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + if (fileName.match(DTS) && sourceFiles && sourceFiles.length == 1 && + path.normalize(sourceFiles[0].fileName) == normalSyntheticIndexName) { + // If we are writing the synthetic index, write the metadata along side. + const metadataName = fileName.replace(DTS, '.metadata.json'); + fs.writeFileSync(metadataName, indexMetadata, {encoding: 'utf8'}); + } + }; + return newHost; +} + +export function createBundleIndexHost( + ngOptions: CompilerOptions, rootFiles: string[], + host: H): {host: H, indexName?: string, errors?: ts.Diagnostic[]} { + const files = rootFiles.filter(f => !DTS.test(f)); + if (files.length != 1) { + return { + host, + errors: [{ + file: null as any as ts.SourceFile, + start: null as any as number, + length: null as any as number, + messageText: + 'Angular compiler option "flatModuleIndex" requires one and only one .ts file in the "files" field.', + category: ts.DiagnosticCategory.Error, + code: 0 + }] + }; + } + const file = files[0]; + const indexModule = file.replace(/\.ts$/, ''); + const bundler = + new MetadataBundler(indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host)); + const metadataBundle = bundler.getMetadataBundle(); + const metadata = JSON.stringify(metadataBundle.metadata); + const name = + path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile !.replace(JS_EXT, '.ts')); + const libraryIndex = `./${path.basename(indexModule)}`; + const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates); + host = createSyntheticIndexHost(host, {name, content, metadata}); + return {host, indexName: name}; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/metadata/bundler.ts b/packages/compiler-cli/src/metadata/bundler.ts new file mode 100644 index 0000000000..6024b68bcf --- /dev/null +++ b/packages/compiler-cli/src/metadata/bundler.ts @@ -0,0 +1,628 @@ +/** + * @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 path from 'path'; +import * as ts from 'typescript'; + +import {MetadataCollector} from '../metadata/collector'; +import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from '../metadata/schema'; + + + +// The character set used to produce private names. +const PRIVATE_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz'; + +interface Symbol { + module: string; + name: string; + + // Produced by indirectly by exportAll() for symbols re-export another symbol. + exports?: Symbol; + + // Produced by indirectly by exportAll() for symbols are re-exported by another symbol. + reexportedAs?: Symbol; + + // Produced by canonicalizeSymbols() for all symbols. A symbol is private if it is not + // exported by the index. + isPrivate?: boolean; + + // Produced by canonicalizeSymbols() for all symbols. This is the one symbol that + // respresents all other symbols and is the only symbol that, among all the re-exported + // aliases, whose fields can be trusted to contain the correct information. + // For private symbols this is the declaration symbol. For public symbols this is the + // symbol that is exported. + canonicalSymbol?: Symbol; + + // Produced by canonicalizeSymbols() for all symbols. This the symbol that originally + // declared the value and should be used to fetch the value. + declaration?: Symbol; + + // A symbol is referenced if it is exported from index or referenced by the value of + // a referenced symbol's value. + referenced?: boolean; + + // A symbol is marked as a re-export the symbol was rexported from a module that is + // not part of the flat module bundle. + reexport?: boolean; + + // Only valid for referenced canonical symbols. Produces by convertSymbols(). + value?: MetadataEntry; + + // Only valid for referenced private symbols. It is the name to use to import the symbol from + // the bundle index. Produce by assignPrivateNames(); + privateName?: string; +} + +export interface BundleEntries { [name: string]: MetadataEntry; } + +export interface BundlePrivateEntry { + privateName: string; + name: string; + module: string; +} + +export interface BundledModule { + metadata: ModuleMetadata; + privates: BundlePrivateEntry[]; +} + +export interface MetadataBundlerHost { + getMetadataFor(moduleName: string): ModuleMetadata|undefined; +} + +type StaticsMetadata = { + [name: string]: MetadataValue | FunctionMetadata; +}; + +export class MetadataBundler { + private symbolMap = new Map(); + private metadataCache = new Map(); + private exports = new Map(); + private rootModule: string; + private exported: Set; + + constructor( + private root: string, private importAs: string|undefined, private host: MetadataBundlerHost) { + this.rootModule = `./${path.basename(root)}`; + } + + getMetadataBundle(): BundledModule { + // Export the root module. This also collects the transitive closure of all values referenced by + // the exports. + const exportedSymbols = this.exportAll(this.rootModule); + this.canonicalizeSymbols(exportedSymbols); + // TODO: exports? e.g. a module re-exports a symbol from another bundle + const metadata = this.getEntries(exportedSymbols); + const privates = Array.from(this.symbolMap.values()) + .filter(s => s.referenced && s.isPrivate) + .map(s => ({ + privateName: s.privateName !, + name: s.declaration !.name, + module: s.declaration !.module + })); + const origins = Array.from(this.symbolMap.values()) + .filter(s => s.referenced && !s.reexport) + .reduce<{[name: string]: string}>((p, s) => { + p[s.isPrivate ? s.privateName ! : s.name] = s.declaration !.module; + return p; + }, {}); + const exports = this.getReExports(exportedSymbols); + return { + metadata: { + __symbolic: 'module', + version: VERSION, + exports: exports.length ? exports : undefined, metadata, origins, + importAs: this.importAs ! + }, + privates + }; + } + + static resolveModule(importName: string, from: string): string { + return resolveModule(importName, from); + } + + private getMetadata(moduleName: string): ModuleMetadata|undefined { + let result = this.metadataCache.get(moduleName); + if (!result) { + if (moduleName.startsWith('.')) { + const fullModuleName = resolveModule(moduleName, this.root); + result = this.host.getMetadataFor(fullModuleName); + } + this.metadataCache.set(moduleName, result); + } + return result; + } + + private exportAll(moduleName: string): Symbol[] { + const module = this.getMetadata(moduleName); + let result = this.exports.get(moduleName); + + if (result) { + return result; + } + + result = []; + + const exportSymbol = (exportedSymbol: Symbol, exportAs: string) => { + const symbol = this.symbolOf(moduleName, exportAs); + result !.push(symbol); + exportedSymbol.reexportedAs = symbol; + symbol.exports = exportedSymbol; + }; + + // Export all the symbols defined in this module. + if (module && module.metadata) { + for (let key in module.metadata) { + const data = module.metadata[key]; + if (isMetadataImportedSymbolReferenceExpression(data)) { + // This is a re-export of an imported symbol. Record this as a re-export. + const exportFrom = resolveModule(data.module, moduleName); + this.exportAll(exportFrom); + const symbol = this.symbolOf(exportFrom, data.name); + exportSymbol(symbol, key); + } else { + // Record that this symbol is exported by this module. + result.push(this.symbolOf(moduleName, key)); + } + } + } + + // Export all the re-exports from this module + if (module && module.exports) { + for (const exportDeclaration of module.exports) { + const exportFrom = resolveModule(exportDeclaration.from, moduleName); + // Record all the exports from the module even if we don't use it directly. + const exportedSymbols = this.exportAll(exportFrom); + if (exportDeclaration.export) { + // Re-export all the named exports from a module. + for (const exportItem of exportDeclaration.export) { + const name = typeof exportItem == 'string' ? exportItem : exportItem.name; + const exportAs = typeof exportItem == 'string' ? exportItem : exportItem.as; + const symbol = this.symbolOf(exportFrom, name); + if (exportedSymbols && exportedSymbols.length == 1 && exportedSymbols[0].reexport && + exportedSymbols[0].name == '*') { + // This is a named export from a module we have no metadata about. Record the named + // export as a re-export. + symbol.reexport = true; + } + exportSymbol(this.symbolOf(exportFrom, name), exportAs); + } + } else { + // Re-export all the symbols from the module + const exportedSymbols = this.exportAll(exportFrom); + for (const exportedSymbol of exportedSymbols) { + const name = exportedSymbol.name; + exportSymbol(exportedSymbol, name); + } + } + } + } + + if (!module) { + // If no metadata is found for this import then it is considered external to the + // library and should be recorded as a re-export in the final metadata if it is + // eventually re-exported. + const symbol = this.symbolOf(moduleName, '*'); + symbol.reexport = true; + result.push(symbol); + } + this.exports.set(moduleName, result); + + return result; + } + + /** + * Fill in the canonicalSymbol which is the symbol that should be imported by factories. + * The canonical symbol is the one exported by the index file for the bundle or definition + * symbol for private symbols that are not exported by bundle index. + */ + private canonicalizeSymbols(exportedSymbols: Symbol[]) { + const symbols = Array.from(this.symbolMap.values()); + this.exported = new Set(exportedSymbols); + symbols.forEach(this.canonicalizeSymbol, this); + } + + private canonicalizeSymbol(symbol: Symbol) { + const rootExport = getRootExport(symbol); + const declaration = getSymbolDeclaration(symbol); + const isPrivate = !this.exported.has(rootExport); + const canonicalSymbol = isPrivate ? declaration : rootExport; + symbol.isPrivate = isPrivate; + symbol.declaration = declaration; + symbol.canonicalSymbol = canonicalSymbol; + symbol.reexport = declaration.reexport; + } + + private getEntries(exportedSymbols: Symbol[]): BundleEntries { + const result: BundleEntries = {}; + + const exportedNames = new Set(exportedSymbols.map(s => s.name)); + let privateName = 0; + + function newPrivateName(): string { + while (true) { + let digits: string[] = []; + let index = privateName++; + let base = PRIVATE_NAME_CHARS; + while (!digits.length || index > 0) { + digits.unshift(base[index % base.length]); + index = Math.floor(index / base.length); + } + digits.unshift('\u0275'); + const result = digits.join(''); + if (!exportedNames.has(result)) return result; + } + } + + exportedSymbols.forEach(symbol => this.convertSymbol(symbol)); + + const symbolsMap = new Map(); + Array.from(this.symbolMap.values()).forEach(symbol => { + if (symbol.referenced && !symbol.reexport) { + let name = symbol.name; + const declaredName = symbol.declaration !.name; + if (symbol.isPrivate && !symbol.privateName) { + name = newPrivateName(); + symbol.privateName = name; + } + if (symbolsMap.has(declaredName)) { + const names = symbolsMap.get(declaredName); + names !.push(name); + } else { + symbolsMap.set(declaredName, [name]); + } + result[name] = symbol.value !; + } + }); + + // check for duplicated entries + symbolsMap.forEach((names: string[], declaredName: string) => { + if (names.length > 1) { + // prefer the export that uses the declared name (if any) + let reference = names.indexOf(declaredName); + if (reference === -1) { + reference = 0; + } + + // keep one entry and replace the others by references + names.forEach((name: string, i: number) => { + if (i !== reference) { + result[name] = {__symbolic: 'reference', name: names[reference]}; + } + }); + } + }); + + return result; + } + + private getReExports(exportedSymbols: Symbol[]): ModuleExportMetadata[] { + type ExportClause = {name: string, as: string}[]; + const modules = new Map(); + const exportAlls = new Set(); + for (const symbol of exportedSymbols) { + if (symbol.reexport) { + // symbol.declaration is guarenteed to be defined during the phase this method is called. + const declaration = symbol.declaration !; + const module = declaration.module; + if (declaration !.name == '*') { + // Reexport all the symbols. + exportAlls.add(declaration.module); + } else { + // Re-export the symbol as the exported name. + let entry = modules.get(module); + if (!entry) { + entry = []; + modules.set(module, entry); + } + const as = symbol.name; + const name = declaration.name; + entry.push({name, as}); + } + } + } + return [ + ...Array.from(exportAlls.values()).map(from => ({from})), + ...Array.from(modules.entries()).map(([from, exports]) => ({export: exports, from})) + ]; + } + + private convertSymbol(symbol: Symbol) { + // canonicalSymbol is ensured to be defined before this is called. + const canonicalSymbol = symbol.canonicalSymbol !; + + if (!canonicalSymbol.referenced) { + canonicalSymbol.referenced = true; + // declaration is ensured to be definded before this method is called. + const declaration = canonicalSymbol.declaration !; + const module = this.getMetadata(declaration.module); + if (module) { + const value = module.metadata[declaration.name]; + if (value && !declaration.name.startsWith('___')) { + canonicalSymbol.value = this.convertEntry(declaration.module, value); + } + } + } + } + + private convertEntry(moduleName: string, value: MetadataEntry): MetadataEntry { + if (isClassMetadata(value)) { + return this.convertClass(moduleName, value); + } + if (isFunctionMetadata(value)) { + return this.convertFunction(moduleName, value); + } + if (isInterfaceMetadata(value)) { + return value; + } + return this.convertValue(moduleName, value); + } + + private convertClass(moduleName: string, value: ClassMetadata): ClassMetadata { + return { + __symbolic: 'class', + arity: value.arity, + extends: this.convertExpression(moduleName, value.extends) !, + decorators: + value.decorators && value.decorators.map(d => this.convertExpression(moduleName, d) !), + members: this.convertMembers(moduleName, value.members !), + statics: value.statics && this.convertStatics(moduleName, value.statics) + }; + } + + private convertMembers(moduleName: string, members: MetadataMap): MetadataMap { + const result: MetadataMap = {}; + for (const name in members) { + const value = members[name]; + result[name] = value.map(v => this.convertMember(moduleName, v)); + } + return result; + } + + private convertMember(moduleName: string, member: MemberMetadata) { + const result: MemberMetadata = {__symbolic: member.__symbolic}; + result.decorators = + member.decorators && member.decorators.map(d => this.convertExpression(moduleName, d) !); + if (isMethodMetadata(member)) { + (result as MethodMetadata).parameterDecorators = member.parameterDecorators && + member.parameterDecorators.map( + d => d && d.map(p => this.convertExpression(moduleName, p) !)); + if (isConstructorMetadata(member)) { + if (member.parameters) { + (result as ConstructorMetadata).parameters = + member.parameters.map(p => this.convertExpression(moduleName, p)); + } + } + } + return result; + } + + private convertStatics(moduleName: string, statics: StaticsMetadata): StaticsMetadata { + let result: StaticsMetadata = {}; + for (const key in statics) { + const value = statics[key]; + result[key] = isFunctionMetadata(value) ? this.convertFunction(moduleName, value) : value; + } + return result; + } + + private convertFunction(moduleName: string, value: FunctionMetadata): FunctionMetadata { + return { + __symbolic: 'function', + parameters: value.parameters, + defaults: value.defaults && value.defaults.map(v => this.convertValue(moduleName, v)), + value: this.convertValue(moduleName, value.value) + }; + } + + private convertValue(moduleName: string, value: MetadataValue): MetadataValue { + if (isPrimitive(value)) { + return value; + } + if (isMetadataError(value)) { + return this.convertError(moduleName, value); + } + if (isMetadataSymbolicExpression(value)) { + return this.convertExpression(moduleName, value) !; + } + if (Array.isArray(value)) { + return value.map(v => this.convertValue(moduleName, v)); + } + + // Otherwise it is a metadata object. + const object = value as MetadataObject; + const result: MetadataObject = {}; + for (const key in object) { + result[key] = this.convertValue(moduleName, object[key]); + } + return result; + } + + private convertExpression( + moduleName: string, value: MetadataSymbolicExpression|MetadataError|null| + undefined): MetadataSymbolicExpression|MetadataError|undefined|null { + if (value) { + switch (value.__symbolic) { + case 'error': + return this.convertError(moduleName, value as MetadataError); + case 'reference': + return this.convertReference(moduleName, value as MetadataSymbolicReferenceExpression); + default: + return this.convertExpressionNode(moduleName, value); + } + } + return value; + } + + private convertError(module: string, value: MetadataError): MetadataError { + return { + __symbolic: 'error', + message: value.message, + line: value.line, + character: value.character, + context: value.context, module + }; + } + + private convertReference(moduleName: string, value: MetadataSymbolicReferenceExpression): + MetadataSymbolicReferenceExpression|MetadataError|undefined { + const createReference = (symbol: Symbol): MetadataSymbolicReferenceExpression => { + const declaration = symbol.declaration !; + if (declaration.module.startsWith('.')) { + // Reference to a symbol defined in the module. Ensure it is converted then return a + // references to the final symbol. + this.convertSymbol(symbol); + return { + __symbolic: 'reference', + get name() { + // Resolved lazily because private names are assigned late. + const canonicalSymbol = symbol.canonicalSymbol !; + if (canonicalSymbol.isPrivate == null) { + throw Error('Invalid state: isPrivate was not initialized'); + } + return canonicalSymbol.isPrivate ? canonicalSymbol.privateName ! : canonicalSymbol.name; + } + }; + } else { + // The symbol was a re-exported symbol from another module. Return a reference to the + // original imported symbol. + return {__symbolic: 'reference', name: declaration.name, module: declaration.module}; + } + }; + + if (isMetadataGlobalReferenceExpression(value)) { + const metadata = this.getMetadata(moduleName); + if (metadata && metadata.metadata && metadata.metadata[value.name]) { + // Reference to a symbol defined in the module + return createReference(this.canonicalSymbolOf(moduleName, value.name)); + } + + // If a reference has arguments, the arguments need to be converted. + if (value.arguments) { + return { + __symbolic: 'reference', + name: value.name, + arguments: value.arguments.map(a => this.convertValue(moduleName, a)) + }; + } + + // Global references without arguments (such as to Math or JSON) are unmodified. + return value; + } + + if (isMetadataImportedSymbolReferenceExpression(value)) { + // References to imported symbols are separated into two, references to bundled modules and + // references to modules external to the bundle. If the module reference is relative it is + // assumed to be in the bundle. If it is Global it is assumed to be outside the bundle. + // References to symbols outside the bundle are left unmodified. References to symbol inside + // the bundle need to be converted to a bundle import reference reachable from the bundle + // index. + + if (value.module.startsWith('.')) { + // Reference is to a symbol defined inside the module. Convert the reference to a reference + // to the canonical symbol. + const referencedModule = resolveModule(value.module, moduleName); + const referencedName = value.name; + return createReference(this.canonicalSymbolOf(referencedModule, referencedName)); + } + + // Value is a reference to a symbol defined outside the module. + if (value.arguments) { + // If a reference has arguments the arguments need to be converted. + return { + __symbolic: 'reference', + name: value.name, + module: value.module, + arguments: value.arguments.map(a => this.convertValue(moduleName, a)) + }; + } + return value; + } + + if (isMetadataModuleReferenceExpression(value)) { + // Cannot support references to bundled modules as the internal modules of a bundle are erased + // by the bundler. + if (value.module.startsWith('.')) { + return { + __symbolic: 'error', + message: 'Unsupported bundled module reference', + context: {module: value.module} + }; + } + + // References to unbundled modules are unmodified. + return value; + } + } + + private convertExpressionNode(moduleName: string, value: MetadataSymbolicExpression): + MetadataSymbolicExpression { + const result: MetadataSymbolicExpression = {__symbolic: value.__symbolic}; + for (const key in value) { + (result as any)[key] = this.convertValue(moduleName, (value as any)[key]); + } + return result; + } + + private symbolOf(module: string, name: string): Symbol { + const symbolKey = `${module}:${name}`; + let symbol = this.symbolMap.get(symbolKey); + if (!symbol) { + symbol = {module, name}; + this.symbolMap.set(symbolKey, symbol); + } + return symbol; + } + + private canonicalSymbolOf(module: string, name: string): Symbol { + // Ensure the module has been seen. + this.exportAll(module); + const symbol = this.symbolOf(module, name); + if (!symbol.canonicalSymbol) { + this.canonicalizeSymbol(symbol); + } + return symbol; + } +} + +export class CompilerHostAdapter implements MetadataBundlerHost { + private collector = new MetadataCollector(); + + constructor(private host: ts.CompilerHost) {} + + getMetadataFor(fileName: string): ModuleMetadata|undefined { + const sourceFile = this.host.getSourceFile(fileName + '.ts', ts.ScriptTarget.Latest); + return this.collector.getMetadata(sourceFile); + } +} + +function resolveModule(importName: string, from: string): string { + if (importName.startsWith('.') && from) { + let normalPath = path.normalize(path.join(path.dirname(from), importName)); + if (!normalPath.startsWith('.') && from.startsWith('.')) { + // path.normalize() preserves leading '../' but not './'. This adds it back. + normalPath = `.${path.sep}${normalPath}`; + } + // Replace windows path delimiters with forward-slashes. Otherwise the paths are not + // TypeScript compatible when building the bundle. + return normalPath.replace(/\\/g, '/'); + } + return importName; +} + +function isPrimitive(o: any): o is boolean|string|number { + return o === null || (typeof o !== 'function' && typeof o !== 'object'); +} + +function getRootExport(symbol: Symbol): Symbol { + return symbol.reexportedAs ? getRootExport(symbol.reexportedAs) : symbol; +} + +function getSymbolDeclaration(symbol: Symbol): Symbol { + return symbol.exports ? getSymbolDeclaration(symbol.exports) : symbol; +} diff --git a/packages/compiler-cli/src/metadata/collector.ts b/packages/compiler-cli/src/metadata/collector.ts new file mode 100644 index 0000000000..b9b5f3a45b --- /dev/null +++ b/packages/compiler-cli/src/metadata/collector.ts @@ -0,0 +1,774 @@ +/** + * @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 {Evaluator, errorSymbol} from './evaluator'; +import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema'; +import {Symbols} from './symbols'; + +// In TypeScript 2.1 these flags moved +// These helpers work for both 2.0 and 2.1. +const isExport = (ts as any).ModifierFlags ? + ((node: ts.Node) => + !!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Export)) : + ((node: ts.Node) => !!((node.flags & (ts as any).NodeFlags.Export))); +const isStatic = (ts as any).ModifierFlags ? + ((node: ts.Node) => + !!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Static)) : + ((node: ts.Node) => !!((node.flags & (ts as any).NodeFlags.Static))); + +/** + * A set of collector options to use when collecting metadata. + */ +export interface CollectorOptions { + /** + * Version of the metadata to collect. + */ + version?: number; + + /** + * Collect a hidden field "$quoted$" in objects literals that record when the key was quoted in + * the source. + */ + quotedNames?: boolean; + + /** + * Do not simplify invalid expressions. + */ + verboseInvalidExpression?: boolean; + + /** + * An expression substitution callback. + */ + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue; +} + +/** + * Collect decorator metadata from a TypeScript module. + */ +export class MetadataCollector { + constructor(private options: CollectorOptions = {}) {} + + /** + * Returns a JSON.stringify friendly form describing the decorators of the exported classes from + * the source file that is expected to correspond to a module. + */ + public getMetadata( + sourceFile: ts.SourceFile, strict: boolean = false, + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue): ModuleMetadata + |undefined { + const locals = new Symbols(sourceFile); + const nodeMap = + new Map(); + const composedSubstituter = substituteExpression && this.options.substituteExpression ? + (value: MetadataValue, node: ts.Node) => + this.options.substituteExpression !(substituteExpression(value, node), node) : + substituteExpression; + const evaluatorOptions = substituteExpression ? + {...this.options, substituteExpression: composedSubstituter} : + this.options; + let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; + const evaluator = new Evaluator(locals, nodeMap, evaluatorOptions, (name, value) => { + if (!metadata) metadata = {}; + metadata[name] = value; + }); + let exports: ModuleExportMetadata[]|undefined = undefined; + + function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { + return evaluator.evaluateNode(decoratorNode.expression); + } + + function recordEntry(entry: T, node: ts.Node): T { + nodeMap.set(entry, node); + return entry; + } + + function errorSym( + message: string, node?: ts.Node, context?: {[name: string]: string}): MetadataError { + return errorSymbol(message, node, context, sourceFile); + } + + function maybeGetSimpleFunction( + functionDeclaration: ts.FunctionDeclaration | + ts.MethodDeclaration): {func: FunctionMetadata, name: string}|undefined { + if (functionDeclaration.name && functionDeclaration.name.kind == ts.SyntaxKind.Identifier) { + const nameNode = functionDeclaration.name; + const functionName = nameNode.text; + const functionBody = functionDeclaration.body; + if (functionBody && functionBody.statements.length == 1) { + const statement = functionBody.statements[0]; + if (statement.kind === ts.SyntaxKind.ReturnStatement) { + const returnStatement = statement; + if (returnStatement.expression) { + const func: FunctionMetadata = { + __symbolic: 'function', + parameters: namesOf(functionDeclaration.parameters), + value: evaluator.evaluateNode(returnStatement.expression) + }; + if (functionDeclaration.parameters.some(p => p.initializer != null)) { + func.defaults = functionDeclaration.parameters.map( + p => p.initializer && evaluator.evaluateNode(p.initializer)); + } + return recordEntry({func, name: functionName}, functionDeclaration); + } + } + } + } + } + + function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata { + const result: ClassMetadata = {__symbolic: 'class'}; + + function getDecorators(decorators: ts.Decorator[] | undefined): MetadataSymbolicExpression[]| + undefined { + if (decorators && decorators.length) + return decorators.map(decorator => objFromDecorator(decorator)); + return undefined; + } + + function referenceFrom(node: ts.Node): MetadataSymbolicReferenceExpression|MetadataError| + MetadataSymbolicSelectExpression { + const result = evaluator.evaluateNode(node); + if (isMetadataError(result) || isMetadataSymbolicReferenceExpression(result) || + isMetadataSymbolicSelectExpression(result)) { + return result; + } else { + return errorSym('Symbol reference expected', node); + } + } + + // Add class parents + if (classDeclaration.heritageClauses) { + classDeclaration.heritageClauses.forEach((hc) => { + if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) { + hc.types.forEach(type => result.extends = referenceFrom(type.expression)); + } + }); + } + + // Add arity if the type is generic + const typeParameters = classDeclaration.typeParameters; + if (typeParameters && typeParameters.length) { + result.arity = typeParameters.length; + } + + // Add class decorators + if (classDeclaration.decorators) { + result.decorators = getDecorators(classDeclaration.decorators); + } + + // member decorators + let members: MetadataMap|null = null; + function recordMember(name: string, metadata: MemberMetadata) { + if (!members) members = {}; + const data = members.hasOwnProperty(name) ? members[name] : []; + data.push(metadata); + members[name] = data; + } + + // static member + let statics: {[name: string]: MetadataValue | FunctionMetadata}|null = null; + function recordStaticMember(name: string, value: MetadataValue | FunctionMetadata) { + if (!statics) statics = {}; + statics[name] = value; + } + + for (const member of classDeclaration.members) { + let isConstructor = false; + switch (member.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.MethodDeclaration: + isConstructor = member.kind === ts.SyntaxKind.Constructor; + const method = member; + if (isStatic(method)) { + const maybeFunc = maybeGetSimpleFunction(method); + if (maybeFunc) { + recordStaticMember(maybeFunc.name, maybeFunc.func); + } + continue; + } + const methodDecorators = getDecorators(method.decorators); + const parameters = method.parameters; + const parameterDecoratorData: + ((MetadataSymbolicExpression | MetadataError)[] | undefined)[] = []; + const parametersData: + (MetadataSymbolicReferenceExpression | MetadataError | + MetadataSymbolicSelectExpression | null)[] = []; + let hasDecoratorData: boolean = false; + let hasParameterData: boolean = false; + for (const parameter of parameters) { + const parameterData = getDecorators(parameter.decorators); + parameterDecoratorData.push(parameterData); + hasDecoratorData = hasDecoratorData || !!parameterData; + if (isConstructor) { + if (parameter.type) { + parametersData.push(referenceFrom(parameter.type)); + } else { + parametersData.push(null); + } + hasParameterData = true; + } + } + const data: MethodMetadata = {__symbolic: isConstructor ? 'constructor' : 'method'}; + const name = isConstructor ? '__ctor__' : evaluator.nameOf(member.name); + if (methodDecorators) { + data.decorators = methodDecorators; + } + if (hasDecoratorData) { + data.parameterDecorators = parameterDecoratorData; + } + if (hasParameterData) { + (data).parameters = parametersData; + } + if (!isMetadataError(name)) { + recordMember(name, data); + } + break; + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + const property = member; + if (isStatic(property)) { + const name = evaluator.nameOf(property.name); + if (!isMetadataError(name)) { + if (property.initializer) { + const value = evaluator.evaluateNode(property.initializer); + recordStaticMember(name, value); + } else { + recordStaticMember(name, errorSym('Variable not initialized', property.name)); + } + } + } + const propertyDecorators = getDecorators(property.decorators); + if (propertyDecorators) { + const name = evaluator.nameOf(property.name); + if (!isMetadataError(name)) { + recordMember(name, {__symbolic: 'property', decorators: propertyDecorators}); + } + } + break; + } + } + if (members) { + result.members = members; + } + if (statics) { + result.statics = statics; + } + + return recordEntry(result, classDeclaration); + } + + // Collect all exported symbols from an exports clause. + const exportMap = new Map(); + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ExportDeclaration: + const exportDeclaration = node; + const {moduleSpecifier, exportClause} = exportDeclaration; + + if (!moduleSpecifier) { + // If there is a module specifier there is also an exportClause + 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) => + identifier && exportMap.has(identifier.text); + const isExported = + (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | + ts.InterfaceDeclaration | ts.EnumDeclaration) => + isExport(node) || isExportedIdentifier(node.name); + const exportedIdentifierName = (identifier?: ts.Identifier) => + identifier && (exportMap.get(identifier.text) || identifier.text); + const exportedName = + (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration | + ts.TypeAliasDeclaration | ts.EnumDeclaration) => exportedIdentifierName(node.name); + + + // Predeclare classes and functions + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + const classDeclaration = node; + if (classDeclaration.name) { + const className = classDeclaration.name.text; + if (isExported(classDeclaration)) { + locals.define( + className, {__symbolic: 'reference', name: exportedName(classDeclaration)}); + } else { + locals.define( + className, errorSym('Reference to non-exported class', node, {className})); + } + } + break; + + case ts.SyntaxKind.InterfaceDeclaration: + const interfaceDeclaration = node; + if (interfaceDeclaration.name) { + const interfaceName = interfaceDeclaration.name.text; + // All references to interfaces should be converted to references to `any`. + locals.define(interfaceName, {__symbolic: 'reference', name: 'any'}); + } + break; + + case ts.SyntaxKind.FunctionDeclaration: + const functionDeclaration = node; + if (!isExported(functionDeclaration)) { + // Report references to this function as an error. + const nameNode = functionDeclaration.name; + if (nameNode && nameNode.text) { + locals.define( + nameNode.text, + errorSym( + 'Reference to a non-exported function', nameNode, {name: nameNode.text})); + } + } + break; + } + }); + + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ExportDeclaration: + // Record export declarations + const exportDeclaration = node; + const {moduleSpecifier, exportClause} = exportDeclaration; + + if (!moduleSpecifier) { + // no module specifier -> export {propName as name}; + if (exportClause) { + exportClause.elements.forEach(spec => { + const name = spec.name.text; + // 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); + } + }); + } + } + + if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) { + // Ignore exports that don't have string literals as exports. + // This is allowed by the syntax but will be flagged as an error by the type checker. + const from = (moduleSpecifier).text; + const moduleExport: ModuleExportMetadata = {from}; + if (exportClause) { + moduleExport.export = exportClause.elements.map( + spec => spec.propertyName ? {name: spec.propertyName.text, as: spec.name.text} : + spec.name.text); + } + if (!exports) exports = []; + exports.push(moduleExport); + } + break; + case ts.SyntaxKind.ClassDeclaration: + const classDeclaration = node; + if (classDeclaration.name) { + if (isExported(classDeclaration)) { + const name = exportedName(classDeclaration); + if (name) { + if (!metadata) metadata = {}; + metadata[name] = classMetadataOf(classDeclaration); + } + } + } + // Otherwise don't record metadata for the class. + break; + + case ts.SyntaxKind.TypeAliasDeclaration: + const typeDeclaration = node; + if (typeDeclaration.name && isExported(typeDeclaration)) { + const name = exportedName(typeDeclaration); + if (name) { + if (!metadata) metadata = {}; + metadata[name] = {__symbolic: 'interface'}; + } + } + break; + + case ts.SyntaxKind.InterfaceDeclaration: + const interfaceDeclaration = node; + if (interfaceDeclaration.name && isExported(interfaceDeclaration)) { + const name = exportedName(interfaceDeclaration); + if (name) { + if (!metadata) metadata = {}; + metadata[name] = {__symbolic: 'interface'}; + } + } + break; + + case ts.SyntaxKind.FunctionDeclaration: + // Record functions that return a single value. Record the parameter + // names substitution will be performed by the StaticReflector. + const functionDeclaration = node; + if (isExported(functionDeclaration) && functionDeclaration.name) { + const name = exportedName(functionDeclaration); + const maybeFunc = maybeGetSimpleFunction(functionDeclaration); + if (name) { + if (!metadata) metadata = {}; + metadata[name] = + maybeFunc ? recordEntry(maybeFunc.func, node) : {__symbolic: 'function'}; + } + } + break; + + case ts.SyntaxKind.EnumDeclaration: + const enumDeclaration = node; + if (isExported(enumDeclaration)) { + const enumValueHolder: {[name: string]: MetadataValue} = {}; + const enumName = exportedName(enumDeclaration); + let nextDefaultValue: MetadataValue = 0; + let writtenMembers = 0; + for (const member of enumDeclaration.members) { + let enumValue: MetadataValue; + if (!member.initializer) { + enumValue = nextDefaultValue; + } else { + enumValue = evaluator.evaluateNode(member.initializer); + } + let name: string|undefined = undefined; + if (member.name.kind == ts.SyntaxKind.Identifier) { + const identifier = member.name; + name = identifier.text; + enumValueHolder[name] = enumValue; + writtenMembers++; + } + if (typeof enumValue === 'number') { + nextDefaultValue = enumValue + 1; + } else if (name) { + nextDefaultValue = { + __symbolic: 'binary', + operator: '+', + left: { + __symbolic: 'select', + expression: recordEntry({__symbolic: 'reference', name: enumName}, node), name + } + }; + } else { + nextDefaultValue = + recordEntry(errorSym('Unsuppported enum member name', member.name), node); + } + } + if (writtenMembers) { + if (enumName) { + if (!metadata) metadata = {}; + metadata[enumName] = recordEntry(enumValueHolder, node); + } + } + } + break; + + case ts.SyntaxKind.VariableStatement: + const variableStatement = node; + for (const variableDeclaration of variableStatement.declarationList.declarations) { + if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) { + const nameNode = variableDeclaration.name; + let varValue: MetadataValue; + if (variableDeclaration.initializer) { + varValue = evaluator.evaluateNode(variableDeclaration.initializer); + } else { + varValue = recordEntry(errorSym('Variable not initialized', nameNode), nameNode); + } + let exported = false; + if (isExport(variableStatement) || isExport(variableDeclaration) || + isExportedIdentifier(nameNode)) { + const name = exportedIdentifierName(nameNode); + if (name) { + if (!metadata) metadata = {}; + metadata[name] = recordEntry(varValue, node); + } + exported = true; + } + if (typeof varValue == 'string' || typeof varValue == 'number' || + typeof varValue == 'boolean') { + locals.define(nameNode.text, varValue); + if (exported) { + locals.defineReference( + nameNode.text, {__symbolic: 'reference', name: nameNode.text}); + } + } else if (!exported) { + if (varValue && !isMetadataError(varValue)) { + locals.define(nameNode.text, recordEntry(varValue, node)); + } else { + locals.define( + nameNode.text, + recordEntry( + errorSym('Reference to a local symbol', nameNode, {name: nameNode.text}), + node)); + } + } + } else { + // Destructuring (or binding) declarations are not supported, + // var {[, ]+} = ; + // or + // var [[, ; + // are not supported. + const report: (nameNode: ts.Node) => void = (nameNode: ts.Node) => { + switch (nameNode.kind) { + case ts.SyntaxKind.Identifier: + const name = nameNode; + const varValue = errorSym('Destructuring not supported', name); + locals.define(name.text, varValue); + if (isExport(node)) { + if (!metadata) metadata = {}; + metadata[name.text] = varValue; + } + break; + case ts.SyntaxKind.BindingElement: + const bindingElement = nameNode; + report(bindingElement.name); + break; + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + const bindings = nameNode; + (bindings as any).elements.forEach(report); + break; + } + }; + report(variableDeclaration.name); + } + } + break; + } + }); + + if (metadata || exports) { + if (!metadata) + metadata = {}; + else if (strict) { + validateMetadata(sourceFile, nodeMap, metadata); + } + const result: ModuleMetadata = { + __symbolic: 'module', + version: this.options.version || VERSION, metadata + }; + if (exports) result.exports = exports; + return result; + } + } +} + +// This will throw if the metadata entry given contains an error node. +function validateMetadata( + sourceFile: ts.SourceFile, nodeMap: Map, + metadata: {[name: string]: MetadataEntry}) { + let locals: Set = new Set(['Array', 'Object', 'Set', 'Map', 'string', 'number', 'any']); + + function validateExpression( + expression: MetadataValue | MetadataSymbolicExpression | MetadataError) { + if (!expression) { + return; + } else if (Array.isArray(expression)) { + expression.forEach(validateExpression); + } else if (typeof expression === 'object' && !expression.hasOwnProperty('__symbolic')) { + Object.getOwnPropertyNames(expression).forEach(v => validateExpression((expression)[v])); + } else if (isMetadataError(expression)) { + reportError(expression); + } else if (isMetadataGlobalReferenceExpression(expression)) { + if (!locals.has(expression.name)) { + const reference = metadata[expression.name]; + if (reference) { + validateExpression(reference); + } + } + } else if (isFunctionMetadata(expression)) { + validateFunction(expression); + } else if (isMetadataSymbolicExpression(expression)) { + switch (expression.__symbolic) { + case 'binary': + const binaryExpression = expression; + validateExpression(binaryExpression.left); + validateExpression(binaryExpression.right); + break; + case 'call': + case 'new': + const callExpression = expression; + validateExpression(callExpression.expression); + if (callExpression.arguments) callExpression.arguments.forEach(validateExpression); + break; + case 'index': + const indexExpression = expression; + validateExpression(indexExpression.expression); + validateExpression(indexExpression.index); + break; + case 'pre': + const prefixExpression = expression; + validateExpression(prefixExpression.operand); + break; + case 'select': + const selectExpression = expression; + validateExpression(selectExpression.expression); + break; + case 'spread': + const spreadExpression = expression; + validateExpression(spreadExpression.expression); + break; + case 'if': + const ifExpression = expression; + validateExpression(ifExpression.condition); + validateExpression(ifExpression.elseExpression); + validateExpression(ifExpression.thenExpression); + break; + } + } + } + + function validateMember(classData: ClassMetadata, member: MemberMetadata) { + if (member.decorators) { + member.decorators.forEach(validateExpression); + } + if (isMethodMetadata(member) && member.parameterDecorators) { + member.parameterDecorators.forEach(validateExpression); + } + // Only validate parameters of classes for which we know that are used with our DI + if (classData.decorators && isConstructorMetadata(member) && member.parameters) { + member.parameters.forEach(validateExpression); + } + } + + function validateClass(classData: ClassMetadata) { + if (classData.decorators) { + classData.decorators.forEach(validateExpression); + } + if (classData.members) { + Object.getOwnPropertyNames(classData.members) + .forEach(name => classData.members ![name].forEach((m) => validateMember(classData, m))); + } + if (classData.statics) { + Object.getOwnPropertyNames(classData.statics).forEach(name => { + const staticMember = classData.statics ![name]; + if (isFunctionMetadata(staticMember)) { + validateExpression(staticMember.value); + } else { + validateExpression(staticMember); + } + }); + } + } + + function validateFunction(functionDeclaration: FunctionMetadata) { + if (functionDeclaration.value) { + const oldLocals = locals; + if (functionDeclaration.parameters) { + locals = new Set(oldLocals.values()); + if (functionDeclaration.parameters) + functionDeclaration.parameters.forEach(n => locals.add(n)); + } + validateExpression(functionDeclaration.value); + locals = oldLocals; + } + } + + function shouldReportNode(node: ts.Node | undefined) { + if (node) { + const nodeStart = node.getStart(); + return !( + node.pos != nodeStart && + sourceFile.text.substring(node.pos, nodeStart).indexOf('@dynamic') >= 0); + } + return true; + } + + function reportError(error: MetadataError) { + const node = nodeMap.get(error); + if (shouldReportNode(node)) { + const lineInfo = error.line != undefined ? + error.character != undefined ? `:${error.line + 1}:${error.character + 1}` : + `:${error.line + 1}` : + ''; + throw new Error( + `${sourceFile.fileName}${lineInfo}: Metadata collected contains an error that will be reported at runtime: ${expandedMessage(error)}.\n ${JSON.stringify(error)}`); + } + } + + Object.getOwnPropertyNames(metadata).forEach(name => { + const entry = metadata[name]; + try { + if (isClassMetadata(entry)) { + validateClass(entry); + } + } catch (e) { + const node = nodeMap.get(entry); + if (shouldReportNode(node)) { + if (node) { + const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + throw new Error( + `${sourceFile.fileName}:${line + 1}:${character + 1}: Error encountered in metadata generated for exported symbol '${name}': \n ${e.message}`); + } + throw new Error( + `Error encountered in metadata generated for exported symbol ${name}: \n ${e.message}`); + } + } + }); +} + +// Collect parameter names from a function. +function namesOf(parameters: ts.NodeArray): string[] { + const result: string[] = []; + + function addNamesOf(name: ts.Identifier | ts.BindingPattern) { + if (name.kind == ts.SyntaxKind.Identifier) { + const identifier = name; + result.push(identifier.text); + } else { + const bindingPattern = name; + for (const element of bindingPattern.elements) { + const name = (element as any).name; + if (name) { + addNamesOf(name); + } + } + } + } + + for (const parameter of parameters) { + addNamesOf(parameter.name); + } + + return result; +} + +function expandedMessage(error: any): string { + switch (error.message) { + case 'Reference to non-exported class': + if (error.context && error.context.className) { + return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`; + } + break; + case 'Variable not initialized': + return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler'; + case 'Destructuring not supported': + return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring'; + case 'Could not resolve type': + if (error.context && error.context.typeName) { + return `Could not resolve type ${error.context.typeName}`; + } + break; + case 'Function call not supported': + let prefix = + error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F'; + return prefix + + 'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function'; + case 'Reference to a local symbol': + if (error.context && error.context.name) { + return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`; + } + } + return error.message; +} diff --git a/packages/compiler-cli/src/metadata/evaluator.ts b/packages/compiler-cli/src/metadata/evaluator.ts new file mode 100644 index 0000000000..db02169a2b --- /dev/null +++ b/packages/compiler-cli/src/metadata/evaluator.ts @@ -0,0 +1,680 @@ +/** + * @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 {CollectorOptions} from './collector'; +import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; +import {Symbols} from './symbols'; + + +// In TypeScript 2.1 the spread element kind was renamed. +const spreadElementSyntaxKind: ts.SyntaxKind = + (ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression; + +function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { + const expression = callExpression.expression; + if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + const propertyAccessExpression = expression; + const name = propertyAccessExpression.name; + if (name.kind == ts.SyntaxKind.Identifier) { + return name.text === memberName; + } + } + return false; +} + +function isCallOf(callExpression: ts.CallExpression, ident: string): boolean { + const expression = callExpression.expression; + if (expression.kind === ts.SyntaxKind.Identifier) { + const identifier = expression; + return identifier.text === ident; + } + return false; +} + +/** + * ts.forEachChild stops iterating children when the callback return a truthy value. + * This method inverts this to implement an `every` style iterator. It will return + * true if every call to `cb` returns `true`. + */ +function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) { + return !ts.forEachChild(node, node => !cb(node)); +} + +export function isPrimitive(value: any): boolean { + return Object(value) !== value; +} + +function isDefined(obj: any): boolean { + return obj !== undefined; +} + +// import {propertyName as name} from 'place' +// import {name} from 'place' +export interface ImportSpecifierMetadata { + name: string; + propertyName?: string; +} +export interface ImportMetadata { + defaultName?: string; // import d from 'place' + namespace?: string; // import * as d from 'place' + namedImports?: ImportSpecifierMetadata[]; // import {a} from 'place' + from: string; // from 'place' +} + + +function getSourceFileOfNode(node: ts.Node | undefined): ts.SourceFile { + while (node && node.kind != ts.SyntaxKind.SourceFile) { + node = node.parent; + } + return node; +} + +/* @internal */ +export function errorSymbol( + message: string, node?: ts.Node, context?: {[name: string]: string}, + sourceFile?: ts.SourceFile): MetadataError { + let result: MetadataError|undefined = undefined; + if (node) { + sourceFile = sourceFile || getSourceFileOfNode(node); + if (sourceFile) { + const {line, character} = + ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)); + result = {__symbolic: 'error', message, line, character}; + } + } + if (!result) { + result = {__symbolic: 'error', message}; + } + if (context) { + result.context = context; + } + return result; +} + +/** + * Produce a symbolic representation of an expression folding values into their final value when + * possible. + */ +export class Evaluator { + constructor( + private symbols: Symbols, private nodeMap: Map, + private options: CollectorOptions = {}, + private recordExport?: (name: string, value: MetadataValue) => void) {} + + nameOf(node: ts.Node|undefined): string|MetadataError { + if (node && node.kind == ts.SyntaxKind.Identifier) { + return (node).text; + } + const result = node && this.evaluateNode(node); + if (isMetadataError(result) || typeof result === 'string') { + return result; + } else { + return errorSymbol( + 'Name expected', node, {received: (node && node.getText()) || ''}); + } + } + + /** + * Returns true if the expression represented by `node` can be folded into a literal expression. + * + * For example, a literal is always foldable. This means that literal expressions such as `1.2` + * `"Some value"` `true` `false` are foldable. + * + * - An object literal is foldable if all the properties in the literal are foldable. + * - An array literal is foldable if all the elements are foldable. + * - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR. + * - A property access is foldable if the object is foldable. + * - A array index is foldable if index expression is foldable and the array is foldable. + * - Binary operator expressions are foldable if the left and right expressions are foldable and + * it is one of '+', '-', '*', '/', '%', '||', and '&&'. + * - An identifier is foldable if a value can be found for its symbol in the evaluator symbol + * table. + */ + public isFoldable(node: ts.Node): boolean { + return this.isFoldableWorker(node, new Map()); + } + + private isFoldableWorker(node: ts.Node|undefined, folding: Map): boolean { + if (node) { + switch (node.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + return everyNodeChild(node, child => { + if (child.kind === ts.SyntaxKind.PropertyAssignment) { + const propertyAssignment = child; + return this.isFoldableWorker(propertyAssignment.initializer, folding); + } + return false; + }); + case ts.SyntaxKind.ArrayLiteralExpression: + return everyNodeChild(node, child => this.isFoldableWorker(child, folding)); + case ts.SyntaxKind.CallExpression: + const callExpression = node; + // We can fold a .concat(). + if (isMethodCallOf(callExpression, 'concat') && + arrayOrEmpty(callExpression.arguments).length === 1) { + const arrayNode = (callExpression.expression).expression; + if (this.isFoldableWorker(arrayNode, folding) && + this.isFoldableWorker(callExpression.arguments[0], folding)) { + // It needs to be an array. + const arrayValue = this.evaluateNode(arrayNode); + if (arrayValue && Array.isArray(arrayValue)) { + return true; + } + } + } + + // We can fold a call to CONST_EXPR + if (isCallOf(callExpression, 'CONST_EXPR') && + arrayOrEmpty(callExpression.arguments).length === 1) + return this.isFoldableWorker(callExpression.arguments[0], folding); + return false; + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateMiddle: + case ts.SyntaxKind.TemplateTail: + return true; + case ts.SyntaxKind.ParenthesizedExpression: + const parenthesizedExpression = node; + return this.isFoldableWorker(parenthesizedExpression.expression, folding); + case ts.SyntaxKind.BinaryExpression: + const binaryExpression = node; + switch (binaryExpression.operatorToken.kind) { + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.MinusToken: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.BarBarToken: + return this.isFoldableWorker(binaryExpression.left, folding) && + this.isFoldableWorker(binaryExpression.right, folding); + default: + return false; + } + case ts.SyntaxKind.PropertyAccessExpression: + const propertyAccessExpression = node; + return this.isFoldableWorker(propertyAccessExpression.expression, folding); + case ts.SyntaxKind.ElementAccessExpression: + const elementAccessExpression = node; + return this.isFoldableWorker(elementAccessExpression.expression, folding) && + this.isFoldableWorker(elementAccessExpression.argumentExpression, folding); + case ts.SyntaxKind.Identifier: + let identifier = node; + let reference = this.symbols.resolve(identifier.text); + if (reference !== undefined && isPrimitive(reference)) { + return true; + } + break; + case ts.SyntaxKind.TemplateExpression: + const templateExpression = node; + return templateExpression.templateSpans.every( + span => this.isFoldableWorker(span.expression, folding)); + } + } + return false; + } + + /** + * Produce a JSON serialiable object representing `node`. The foldable values in the expression + * tree are folded. For example, a node representing `1 + 2` is folded into `3`. + */ + public evaluateNode(node: ts.Node, preferReference?: boolean): MetadataValue { + const t = this; + let error: MetadataError|undefined; + + function recordEntry(entry: MetadataValue, node: ts.Node): MetadataValue { + if (t.options.substituteExpression) { + const newEntry = t.options.substituteExpression(entry, node); + if (t.recordExport && newEntry != entry && isMetadataGlobalReferenceExpression(newEntry)) { + t.recordExport(newEntry.name, entry); + } + entry = newEntry; + } + t.nodeMap.set(entry, node); + return entry; + } + + function isFoldableError(value: any): value is MetadataError { + return !t.options.verboseInvalidExpression && isMetadataError(value); + } + + const resolveName = (name: string, preferReference?: boolean): MetadataValue => { + const reference = this.symbols.resolve(name, preferReference); + if (reference === undefined) { + // Encode as a global reference. StaticReflector will check the reference. + return recordEntry({__symbolic: 'reference', name}, node); + } + return reference; + }; + + switch (node.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + let obj: {[name: string]: any} = {}; + let quoted: string[] = []; + ts.forEachChild(node, child => { + switch (child.kind) { + case ts.SyntaxKind.ShorthandPropertyAssignment: + case ts.SyntaxKind.PropertyAssignment: + const assignment = child; + if (assignment.name.kind == ts.SyntaxKind.StringLiteral) { + const name = (assignment.name as ts.StringLiteral).text; + quoted.push(name); + } + const propertyName = this.nameOf(assignment.name); + if (isFoldableError(propertyName)) { + error = propertyName; + return true; + } + const propertyValue = isPropertyAssignment(assignment) ? + this.evaluateNode(assignment.initializer, /* preferReference */ true) : + resolveName(propertyName, /* preferReference */ true); + if (isFoldableError(propertyValue)) { + error = propertyValue; + return true; // Stop the forEachChild. + } else { + obj[propertyName] = isPropertyAssignment(assignment) ? + recordEntry(propertyValue, assignment.initializer) : + propertyValue; + } + } + }); + if (error) return error; + if (this.options.quotedNames && quoted.length) { + obj['$quoted$'] = quoted; + } + return recordEntry(obj, node); + case ts.SyntaxKind.ArrayLiteralExpression: + let arr: MetadataValue[] = []; + ts.forEachChild(node, child => { + const value = this.evaluateNode(child, /* preferReference */ true); + + // Check for error + if (isFoldableError(value)) { + error = value; + return true; // Stop the forEachChild. + } + + // Handle spread expressions + if (isMetadataSymbolicSpreadExpression(value)) { + if (Array.isArray(value.expression)) { + for (const spreadValue of value.expression) { + arr.push(spreadValue); + } + return; + } + } + + arr.push(value); + }); + if (error) return error; + return recordEntry(arr, node); + case spreadElementSyntaxKind: + let spreadExpression = this.evaluateNode((node as any).expression); + return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node); + case ts.SyntaxKind.CallExpression: + const callExpression = node; + if (isCallOf(callExpression, 'forwardRef') && + arrayOrEmpty(callExpression.arguments).length === 1) { + const firstArgument = callExpression.arguments[0]; + if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) { + const arrowFunction = firstArgument; + return recordEntry(this.evaluateNode(arrowFunction.body), node); + } + } + const args = arrayOrEmpty(callExpression.arguments).map(arg => this.evaluateNode(arg)); + if (!this.options.verboseInvalidExpression && args.some(isMetadataError)) { + return args.find(isMetadataError); + } + if (this.isFoldable(callExpression)) { + if (isMethodCallOf(callExpression, 'concat')) { + const arrayValue = this.evaluateNode( + (callExpression.expression).expression); + if (isFoldableError(arrayValue)) return arrayValue; + return arrayValue.concat(args[0]); + } + } + // Always fold a CONST_EXPR even if the argument is not foldable. + if (isCallOf(callExpression, 'CONST_EXPR') && + arrayOrEmpty(callExpression.arguments).length === 1) { + return recordEntry(args[0], node); + } + const expression = this.evaluateNode(callExpression.expression); + if (isFoldableError(expression)) { + return recordEntry(expression, node); + } + let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression}; + if (args && args.length) { + result.arguments = args; + } + return recordEntry(result, node); + case ts.SyntaxKind.NewExpression: + const newExpression = node; + const newArgs = arrayOrEmpty(newExpression.arguments).map(arg => this.evaluateNode(arg)); + if (!this.options.verboseInvalidExpression && newArgs.some(isMetadataError)) { + return recordEntry(newArgs.find(isMetadataError), node); + } + const newTarget = this.evaluateNode(newExpression.expression); + if (isMetadataError(newTarget)) { + return recordEntry(newTarget, node); + } + const call: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget}; + if (newArgs.length) { + call.arguments = newArgs; + } + return recordEntry(call, node); + case ts.SyntaxKind.PropertyAccessExpression: { + const propertyAccessExpression = node; + const expression = this.evaluateNode(propertyAccessExpression.expression); + if (isFoldableError(expression)) { + return recordEntry(expression, node); + } + const member = this.nameOf(propertyAccessExpression.name); + if (isFoldableError(member)) { + return recordEntry(member, node); + } + if (expression && this.isFoldable(propertyAccessExpression.expression)) + return (expression)[member]; + if (isMetadataModuleReferenceExpression(expression)) { + // A select into a module reference and be converted into a reference to the symbol + // in the module + return recordEntry( + {__symbolic: 'reference', module: expression.module, name: member}, node); + } + return recordEntry({__symbolic: 'select', expression, member}, node); + } + case ts.SyntaxKind.ElementAccessExpression: { + const elementAccessExpression = node; + const expression = this.evaluateNode(elementAccessExpression.expression); + if (isFoldableError(expression)) { + return recordEntry(expression, node); + } + if (!elementAccessExpression.argumentExpression) { + return recordEntry(errorSymbol('Expression form not supported', node), node); + } + const index = this.evaluateNode(elementAccessExpression.argumentExpression); + if (isFoldableError(expression)) { + return recordEntry(expression, node); + } + if (this.isFoldable(elementAccessExpression.expression) && + this.isFoldable(elementAccessExpression.argumentExpression)) + return (expression)[index]; + return recordEntry({__symbolic: 'index', expression, index}, node); + } + case ts.SyntaxKind.Identifier: + const identifier = node; + const name = identifier.text; + return resolveName(name, preferReference); + case ts.SyntaxKind.TypeReference: + const typeReferenceNode = node; + const typeNameNode = typeReferenceNode.typeName; + const getReference: (typeNameNode: ts.Identifier | ts.QualifiedName) => MetadataValue = + node => { + if (typeNameNode.kind === ts.SyntaxKind.QualifiedName) { + const qualifiedName = node; + const left = this.evaluateNode(qualifiedName.left); + if (isMetadataModuleReferenceExpression(left)) { + return recordEntry( + { + __symbolic: 'reference', + module: left.module, + name: qualifiedName.right.text + }, + node); + } + // Record a type reference to a declared type as a select. + return {__symbolic: 'select', expression: left, member: qualifiedName.right.text}; + } else { + const identifier = typeNameNode; + const symbol = this.symbols.resolve(identifier.text); + if (isFoldableError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) { + return recordEntry(symbol, node); + } + return recordEntry( + errorSymbol('Could not resolve type', node, {typeName: identifier.text}), node); + } + }; + const typeReference = getReference(typeNameNode); + if (isFoldableError(typeReference)) { + return recordEntry(typeReference, node); + } + if (!isMetadataModuleReferenceExpression(typeReference) && + typeReferenceNode.typeArguments && typeReferenceNode.typeArguments.length) { + const args = typeReferenceNode.typeArguments.map(element => this.evaluateNode(element)); + // TODO: Remove typecast when upgraded to 2.0 as it will be corretly inferred. + // Some versions of 1.9 do not infer this correctly. + (typeReference).arguments = args; + } + return recordEntry(typeReference, node); + case ts.SyntaxKind.UnionType: + const unionType = node; + + // Remove null and undefined from the list of unions. + const references = unionType.types + .filter( + n => n.kind != ts.SyntaxKind.NullKeyword && + n.kind != ts.SyntaxKind.UndefinedKeyword) + .map(n => this.evaluateNode(n)); + + // The remmaining reference must be the same. If two have type arguments consider them + // different even if the type arguments are the same. + let candidate: any = null; + for (let i = 0; i < references.length; i++) { + const reference = references[i]; + if (isMetadataSymbolicReferenceExpression(reference)) { + if (candidate) { + if ((reference as any).name == candidate.name && + (reference as any).module == candidate.module && !(reference as any).arguments) { + candidate = reference; + } + } else { + candidate = reference; + } + } else { + return reference; + } + } + if (candidate) return candidate; + break; + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateTail: + case ts.SyntaxKind.TemplateMiddle: + return (node).text; + case ts.SyntaxKind.NumericLiteral: + return parseFloat((node).text); + case ts.SyntaxKind.AnyKeyword: + return recordEntry({__symbolic: 'reference', name: 'any'}, node); + case ts.SyntaxKind.StringKeyword: + return recordEntry({__symbolic: 'reference', name: 'string'}, node); + case ts.SyntaxKind.NumberKeyword: + return recordEntry({__symbolic: 'reference', name: 'number'}, node); + case ts.SyntaxKind.BooleanKeyword: + return recordEntry({__symbolic: 'reference', name: 'boolean'}, node); + case ts.SyntaxKind.ArrayType: + const arrayTypeNode = node; + return recordEntry( + { + __symbolic: 'reference', + name: 'Array', + arguments: [this.evaluateNode(arrayTypeNode.elementType)] + }, + node); + case ts.SyntaxKind.NullKeyword: + return null; + case ts.SyntaxKind.TrueKeyword: + return true; + case ts.SyntaxKind.FalseKeyword: + return false; + case ts.SyntaxKind.ParenthesizedExpression: + const parenthesizedExpression = node; + return this.evaluateNode(parenthesizedExpression.expression); + case ts.SyntaxKind.TypeAssertionExpression: + const typeAssertion = node; + return this.evaluateNode(typeAssertion.expression); + case ts.SyntaxKind.PrefixUnaryExpression: + const prefixUnaryExpression = node; + const operand = this.evaluateNode(prefixUnaryExpression.operand); + if (isDefined(operand) && isPrimitive(operand)) { + switch (prefixUnaryExpression.operator) { + case ts.SyntaxKind.PlusToken: + return +(operand as any); + case ts.SyntaxKind.MinusToken: + return -(operand as any); + case ts.SyntaxKind.TildeToken: + return ~(operand as any); + case ts.SyntaxKind.ExclamationToken: + return !operand; + } + } + let operatorText: string; + switch (prefixUnaryExpression.operator) { + case ts.SyntaxKind.PlusToken: + operatorText = '+'; + break; + case ts.SyntaxKind.MinusToken: + operatorText = '-'; + break; + case ts.SyntaxKind.TildeToken: + operatorText = '~'; + break; + case ts.SyntaxKind.ExclamationToken: + operatorText = '!'; + break; + default: + return undefined; + } + return recordEntry({__symbolic: 'pre', operator: operatorText, operand: operand}, node); + case ts.SyntaxKind.BinaryExpression: + const binaryExpression = node; + const left = this.evaluateNode(binaryExpression.left); + const right = this.evaluateNode(binaryExpression.right); + if (isDefined(left) && isDefined(right)) { + if (isPrimitive(left) && isPrimitive(right)) + switch (binaryExpression.operatorToken.kind) { + case ts.SyntaxKind.BarBarToken: + return left || right; + case ts.SyntaxKind.AmpersandAmpersandToken: + return left && right; + case ts.SyntaxKind.AmpersandToken: + return left & right; + case ts.SyntaxKind.BarToken: + return left | right; + case ts.SyntaxKind.CaretToken: + return left ^ right; + case ts.SyntaxKind.EqualsEqualsToken: + return left == right; + case ts.SyntaxKind.ExclamationEqualsToken: + return left != right; + case ts.SyntaxKind.EqualsEqualsEqualsToken: + return left === right; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return left !== right; + case ts.SyntaxKind.LessThanToken: + return left < right; + case ts.SyntaxKind.GreaterThanToken: + return left > right; + case ts.SyntaxKind.LessThanEqualsToken: + return left <= right; + case ts.SyntaxKind.GreaterThanEqualsToken: + return left >= right; + case ts.SyntaxKind.LessThanLessThanToken: + return (left) << (right); + case ts.SyntaxKind.GreaterThanGreaterThanToken: + return left >> right; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return left >>> right; + case ts.SyntaxKind.PlusToken: + return left + right; + case ts.SyntaxKind.MinusToken: + return left - right; + case ts.SyntaxKind.AsteriskToken: + return left * right; + case ts.SyntaxKind.SlashToken: + return left / right; + case ts.SyntaxKind.PercentToken: + return left % right; + } + return recordEntry( + { + __symbolic: 'binop', + operator: binaryExpression.operatorToken.getText(), + left: left, + right: right + }, + node); + } + break; + case ts.SyntaxKind.ConditionalExpression: + const conditionalExpression = node; + const condition = this.evaluateNode(conditionalExpression.condition); + const thenExpression = this.evaluateNode(conditionalExpression.whenTrue); + const elseExpression = this.evaluateNode(conditionalExpression.whenFalse); + if (isPrimitive(condition)) { + return condition ? thenExpression : elseExpression; + } + return recordEntry({__symbolic: 'if', condition, thenExpression, elseExpression}, node); + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: + return recordEntry(errorSymbol('Function call not supported', node), node); + case ts.SyntaxKind.TaggedTemplateExpression: + return recordEntry( + errorSymbol('Tagged template expressions are not supported in metadata', node), node); + case ts.SyntaxKind.TemplateExpression: + const templateExpression = node; + if (this.isFoldable(node)) { + return templateExpression.templateSpans.reduce( + (previous, current) => previous + this.evaluateNode(current.expression) + + this.evaluateNode(current.literal), + this.evaluateNode(templateExpression.head)); + } else { + return templateExpression.templateSpans.reduce((previous, current) => { + const expr = this.evaluateNode(current.expression); + const literal = this.evaluateNode(current.literal); + if (isFoldableError(expr)) return expr; + if (isFoldableError(literal)) return literal; + if (typeof previous === 'string' && typeof expr === 'string' && + typeof literal === 'string') { + return previous + expr + literal; + } + let result = expr; + if (previous !== '') { + result = {__symbolic: 'binop', operator: '+', left: previous, right: expr}; + } + if (literal != '') { + result = {__symbolic: 'binop', operator: '+', left: result, right: literal}; + } + return result; + }, this.evaluateNode(templateExpression.head)); + } + case ts.SyntaxKind.AsExpression: + const asExpression = node; + return this.evaluateNode(asExpression.expression); + case ts.SyntaxKind.ClassExpression: + return {__symbolic: 'class'}; + } + return recordEntry(errorSymbol('Expression form not supported', node), node); + } +} + +function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { + return node.kind == ts.SyntaxKind.PropertyAssignment; +} + +const empty = ts.createNodeArray(); + +function arrayOrEmpty(v: ts.NodeArray| undefined): ts.NodeArray { + return v || empty; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/metadata/index.ts b/packages/compiler-cli/src/metadata/index.ts new file mode 100644 index 0000000000..d8aa3ff8f7 --- /dev/null +++ b/packages/compiler-cli/src/metadata/index.ts @@ -0,0 +1,11 @@ +/** + * @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 + */ + +export * from './collector'; +export * from './schema'; +export * from './bundle_index_host'; \ No newline at end of file diff --git a/packages/compiler-cli/src/metadata/index_writer.ts b/packages/compiler-cli/src/metadata/index_writer.ts new file mode 100644 index 0000000000..2c209f1c5b --- /dev/null +++ b/packages/compiler-cli/src/metadata/index_writer.ts @@ -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 {BundlePrivateEntry} from './bundler'; + +const INDEX_HEADER = `/** + * Generated bundle index. Do not edit. + */ +`; + +type MapEntry = [string, BundlePrivateEntry[]]; + +export function privateEntriesToIndex(index: string, privates: BundlePrivateEntry[]): string { + const results: string[] = [INDEX_HEADER]; + + // Export all of the index symbols. + results.push(`export * from '${index}';`, ''); + + // Simplify the exports + const exports = new Map(); + + for (const entry of privates) { + let entries = exports.get(entry.module); + if (!entries) { + entries = []; + exports.set(entry.module, entries); + } + entries.push(entry); + } + + + const compareEntries = compare((e: BundlePrivateEntry) => e.name); + const compareModules = compare((e: MapEntry) => e[0]); + const orderedExports = + Array.from(exports) + .map(([module, entries]) => [module, entries.sort(compareEntries)]) + .sort(compareModules); + + for (const [module, entries] of orderedExports) { + let symbols = entries.map(e => `${e.name} as ${e.privateName}`); + results.push(`export {${symbols}} from '${module}';`); + } + + return results.join('\n'); +} + +function compare(select: (e: E) => T): (a: E, b: E) => number { + return (a, b) => { + const ak = select(a); + const bk = select(b); + return ak > bk ? 1 : ak < bk ? -1 : 0; + }; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/metadata/schema.ts b/packages/compiler-cli/src/metadata/schema.ts new file mode 100644 index 0000000000..20fcb9932b --- /dev/null +++ b/packages/compiler-cli/src/metadata/schema.ts @@ -0,0 +1,284 @@ +/** + * @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 + */ + +// Metadata Schema + +// If you make a backwards incompatible change to the schema, increment the VERSION number. + +// If you make a backwards compatible change to the metadata (such as adding an option field) then +// leave VERSION the same. If possible, as many versions of the metadata that can represent the +// semantics of the file in an array. For example, when generating a version 2 file, if version 1 +// can accurately represent the metadata, generate both version 1 and version 2 in an array. + +export const VERSION = 3; + +export type MetadataEntry = ClassMetadata | InterfaceMetadata | FunctionMetadata | MetadataValue; + +export interface ModuleMetadata { + __symbolic: 'module'; + version: number; + exports?: ModuleExportMetadata[]; + importAs?: string; + metadata: {[name: string]: MetadataEntry}; + origins?: {[name: string]: string}; +} +export function isModuleMetadata(value: any): value is ModuleMetadata { + return value && value.__symbolic === 'module'; +} + +export interface ModuleExportMetadata { + export?: (string|{name: string, as: string})[]; + from: string; +} + +export interface ClassMetadata { + __symbolic: 'class'; + extends?: MetadataSymbolicExpression|MetadataError; + arity?: number; + decorators?: (MetadataSymbolicExpression|MetadataError)[]; + members?: MetadataMap; + statics?: {[name: string]: MetadataValue | FunctionMetadata}; +} +export function isClassMetadata(value: any): value is ClassMetadata { + return value && value.__symbolic === 'class'; +} + +export interface InterfaceMetadata { __symbolic: 'interface'; } +export function isInterfaceMetadata(value: any): value is InterfaceMetadata { + return value && value.__symbolic === 'interface'; +} + +export interface MetadataMap { [name: string]: MemberMetadata[]; } + +export interface MemberMetadata { + __symbolic: 'constructor'|'method'|'property'; + decorators?: (MetadataSymbolicExpression|MetadataError)[]; +} +export function isMemberMetadata(value: any): value is MemberMetadata { + if (value) { + switch (value.__symbolic) { + case 'constructor': + case 'method': + case 'property': + return true; + } + } + return false; +} + +export interface MethodMetadata extends MemberMetadata { + __symbolic: 'constructor'|'method'; + parameterDecorators?: ((MetadataSymbolicExpression | MetadataError)[]|undefined)[]; +} +export function isMethodMetadata(value: any): value is MethodMetadata { + return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method'); +} + +export interface ConstructorMetadata extends MethodMetadata { + __symbolic: 'constructor'; + parameters?: (MetadataSymbolicExpression|MetadataError|null|undefined)[]; +} +export function isConstructorMetadata(value: any): value is ConstructorMetadata { + return value && value.__symbolic === 'constructor'; +} + +export interface FunctionMetadata { + __symbolic: 'function'; + parameters: string[]; + defaults?: MetadataValue[]; + value: MetadataValue; +} +export function isFunctionMetadata(value: any): value is FunctionMetadata { + return value && value.__symbolic === 'function'; +} + +export type MetadataValue = string | number | boolean | undefined | null | MetadataObject | + MetadataArray | MetadataSymbolicExpression | MetadataError; + +export interface MetadataObject { [name: string]: MetadataValue; } + +export interface MetadataArray { [name: number]: MetadataValue; } + +export interface MetadataSymbolicExpression { + __symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'|'spread'|'if'; +} +export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression { + if (value) { + switch (value.__symbolic) { + case 'binary': + case 'call': + case 'index': + case 'new': + case 'pre': + case 'reference': + case 'select': + case 'spread': + case 'if': + return true; + } + } + return false; +} + +export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression { + __symbolic: 'binary'; + operator: '&&'|'||'|'|'|'^'|'&'|'=='|'!='|'==='|'!=='|'<'|'>'|'<='|'>='|'instanceof'|'in'|'as'| + '<<'|'>>'|'>>>'|'+'|'-'|'*'|'/'|'%'|'**'; + left: MetadataValue; + right: MetadataValue; +} +export function isMetadataSymbolicBinaryExpression(value: any): + value is MetadataSymbolicBinaryExpression { + return value && value.__symbolic === 'binary'; +} + +export interface MetadataSymbolicIndexExpression extends MetadataSymbolicExpression { + __symbolic: 'index'; + expression: MetadataValue; + index: MetadataValue; +} +export function isMetadataSymbolicIndexExpression(value: any): + value is MetadataSymbolicIndexExpression { + return value && value.__symbolic === 'index'; +} + +export interface MetadataSymbolicCallExpression extends MetadataSymbolicExpression { + __symbolic: 'call'|'new'; + expression: MetadataValue; + arguments?: MetadataValue[]; +} +export function isMetadataSymbolicCallExpression(value: any): + value is MetadataSymbolicCallExpression { + return value && (value.__symbolic === 'call' || value.__symbolic === 'new'); +} + +export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression { + __symbolic: 'pre'; + operator: '+'|'-'|'~'|'!'; + operand: MetadataValue; +} +export function isMetadataSymbolicPrefixExpression(value: any): + value is MetadataSymbolicPrefixExpression { + return value && value.__symbolic === 'pre'; +} + +export interface MetadataSymbolicIfExpression extends MetadataSymbolicExpression { + __symbolic: 'if'; + condition: MetadataValue; + thenExpression: MetadataValue; + elseExpression: MetadataValue; +} +export function isMetadataSymbolicIfExpression(value: any): value is MetadataSymbolicIfExpression { + return value && value.__symbolic === 'if'; +} + +export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + name: string; + arguments?: MetadataValue[]; +} +export function isMetadataGlobalReferenceExpression(value: any): + value is MetadataGlobalReferenceExpression { + return value && value.name && !value.module && isMetadataSymbolicReferenceExpression(value); +} + +export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + module: string; +} +export function isMetadataModuleReferenceExpression(value: any): + value is MetadataModuleReferenceExpression { + return value && value.module && !value.name && !value.default && + isMetadataSymbolicReferenceExpression(value); +} + +export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + module: string; + name: string; + arguments?: MetadataValue[]; +} +export function isMetadataImportedSymbolReferenceExpression(value: any): + value is MetadataImportedSymbolReferenceExpression { + return value && value.module && !!value.name && isMetadataSymbolicReferenceExpression(value); +} + +export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression { + __symbolic: 'reference'; + module: string; + default: + boolean; + arguments?: MetadataValue[]; +} +export function isMetadataImportDefaultReference(value: any): + value is MetadataImportedDefaultReferenceExpression { + return value.module && value.default && isMetadataSymbolicReferenceExpression(value); +} + +export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression | + MetadataModuleReferenceExpression | MetadataImportedSymbolReferenceExpression | + MetadataImportedDefaultReferenceExpression; +export function isMetadataSymbolicReferenceExpression(value: any): + value is MetadataSymbolicReferenceExpression { + return value && value.__symbolic === 'reference'; +} + +export interface MetadataSymbolicSelectExpression extends MetadataSymbolicExpression { + __symbolic: 'select'; + expression: MetadataValue; + name: string; +} +export function isMetadataSymbolicSelectExpression(value: any): + value is MetadataSymbolicSelectExpression { + return value && value.__symbolic === 'select'; +} + +export interface MetadataSymbolicSpreadExpression extends MetadataSymbolicExpression { + __symbolic: 'spread'; + expression: MetadataValue; +} +export function isMetadataSymbolicSpreadExpression(value: any): + value is MetadataSymbolicSpreadExpression { + return value && value.__symbolic === 'spread'; +} + +export interface MetadataError { + __symbolic: 'error'; + + /** + * This message should be short and relatively discriptive and should be fixed once it is created. + * If the reader doesn't recognize the message, it will display the message unmodified. If the + * reader recognizes the error message is it free to use substitute message the is more + * descriptive and/or localized. + */ + message: string; + + /** + * The line number of the error in the .ts file the metadata was created for. + */ + line?: number; + + /** + * The number of utf8 code-units from the beginning of the file of the error. + */ + character?: number; + + /** + * The module of the error (only used in bundled metadata) + */ + module?: string; + + /** + * Context information that can be used to generate a more descriptive error message. The content + * of the context is dependent on the error message. + */ + context?: {[name: string]: string}; +} +export function isMetadataError(value: any): value is MetadataError { + return value && value.__symbolic === 'error'; +} diff --git a/packages/compiler-cli/src/metadata/symbols.ts b/packages/compiler-cli/src/metadata/symbols.ts new file mode 100644 index 0000000000..6e29dd267f --- /dev/null +++ b/packages/compiler-cli/src/metadata/symbols.ts @@ -0,0 +1,130 @@ +/** + * @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 {MetadataSymbolicReferenceExpression, MetadataValue} from './schema'; + +export class Symbols { + private _symbols: Map; + private references = new Map(); + + constructor(private sourceFile: ts.SourceFile) {} + + resolve(name: string, preferReference?: boolean): MetadataValue|undefined { + return (preferReference && this.references.get(name)) || this.symbols.get(name); + } + + define(name: string, value: MetadataValue) { this.symbols.set(name, value); } + defineReference(name: string, value: MetadataSymbolicReferenceExpression) { + this.references.set(name, value); + } + + has(name: string): boolean { return this.symbols.has(name); } + + private get symbols(): Map { + let result = this._symbols; + if (!result) { + result = this._symbols = new Map(); + populateBuiltins(result); + this.buildImports(); + } + return result; + } + + private buildImports(): void { + const symbols = this._symbols; + // Collect the imported symbols into this.symbols + const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, ''); + const visit = (node: ts.Node) => { + switch (node.kind) { + case ts.SyntaxKind.ImportEqualsDeclaration: + const importEqualsDeclaration = node; + if (importEqualsDeclaration.moduleReference.kind === + ts.SyntaxKind.ExternalModuleReference) { + const externalReference = + importEqualsDeclaration.moduleReference; + if (externalReference.expression) { + // An `import = require(); + if (!externalReference.expression.parent) { + // The `parent` field of a node is set by the TypeScript binder (run as + // part of the type checker). Setting it here allows us to call `getText()` + // even if the `SourceFile` was not type checked (which looks for `SourceFile` + // in the parent chain). This doesn't damage the node as the binder unconditionally + // sets the parent. + externalReference.expression.parent = externalReference; + externalReference.parent = this.sourceFile as any; + } + const from = stripQuotes(externalReference.expression.getText()); + symbols.set( + importEqualsDeclaration.name.text, {__symbolic: 'reference', module: from}); + break; + } + } + symbols.set( + importEqualsDeclaration.name.text, + {__symbolic: 'error', message: `Unsupported import syntax`}); + break; + case ts.SyntaxKind.ImportDeclaration: + const importDecl = node; + if (!importDecl.importClause) { + // An `import ` clause which does not bring symbols into scope. + break; + } + if (!importDecl.moduleSpecifier.parent) { + // See note above in the `ImportEqualDeclaration` case. + importDecl.moduleSpecifier.parent = importDecl; + importDecl.parent = this.sourceFile; + } + const from = stripQuotes(importDecl.moduleSpecifier.getText()); + if (importDecl.importClause.name) { + // An `import form ` clause. Record the defualt symbol. + symbols.set( + importDecl.importClause.name.text, + {__symbolic: 'reference', module: from, default: true}); + } + const bindings = importDecl.importClause.namedBindings; + if (bindings) { + switch (bindings.kind) { + case ts.SyntaxKind.NamedImports: + // An `import { [ [, ] } from ` clause + for (const binding of (bindings).elements) { + symbols.set(binding.name.text, { + __symbolic: 'reference', + module: from, + name: binding.propertyName ? binding.propertyName.text : binding.name.text + }); + } + break; + case ts.SyntaxKind.NamespaceImport: + // An `input * as from ` clause. + symbols.set( + (bindings).name.text, + {__symbolic: 'reference', module: from}); + break; + } + } + break; + } + ts.forEachChild(node, visit); + }; + if (this.sourceFile) { + ts.forEachChild(this.sourceFile, visit); + } + } +} + +function populateBuiltins(symbols: Map) { + // From lib.core.d.ts (all "define const") + ['Object', 'Function', 'String', 'Number', 'Array', 'Boolean', 'Map', 'NaN', 'Infinity', 'Math', + 'Date', 'RegExp', 'Error', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', + 'TypeError', 'URIError', 'JSON', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', + 'Uint8ClampedArray', 'Uint16Array', 'Int16Array', 'Int32Array', 'Uint32Array', 'Float32Array', + 'Float64Array'] + .forEach(name => symbols.set(name, {__symbolic: 'reference', name})); +} diff --git a/packages/compiler-cli/src/ngtools_api.ts b/packages/compiler-cli/src/ngtools_api.ts index eff9d22509..97b85e0472 100644 --- a/packages/compiler-cli/src/ngtools_api.ts +++ b/packages/compiler-cli/src/ngtools_api.ts @@ -14,7 +14,6 @@ */ import {AotCompilerHost, AotSummaryResolver, StaticReflector, StaticSymbolCache, StaticSymbolResolver} from '@angular/compiler'; -import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; import {CodeGenerator} from './codegen'; @@ -22,6 +21,7 @@ import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './ import {Extractor} from './extractor'; import {listLazyRoutesOfModule} from './ngtools_impl'; import {PathMappedCompilerHost} from './path_mapped_compiler_host'; +import {CompilerOptions} from './transformers/api'; export interface NgTools_InternalApi_NG2_CodeGen_Options { basePath: string; @@ -29,7 +29,7 @@ export interface NgTools_InternalApi_NG2_CodeGen_Options { program: ts.Program; host: ts.CompilerHost; - angularCompilerOptions: AngularCompilerOptions; + angularCompilerOptions: CompilerOptions; // i18n options. i18nFormat?: string; @@ -45,7 +45,7 @@ export interface NgTools_InternalApi_NG2_CodeGen_Options { export interface NgTools_InternalApi_NG2_ListLazyRoutes_Options { program: ts.Program; host: ts.CompilerHost; - angularCompilerOptions: AngularCompilerOptions; + angularCompilerOptions: CompilerOptions; entryModule: string; // Every new property under this line should be optional. @@ -58,7 +58,7 @@ export interface NgTools_InternalApi_NG2_ExtractI18n_Options { compilerOptions: ts.CompilerOptions; program: ts.Program; host: ts.CompilerHost; - angularCompilerOptions: AngularCompilerOptions; + angularCompilerOptions: CompilerOptions; i18nFormat?: string; readResource: (fileName: string) => Promise; // Every new property under this line should be optional. diff --git a/packages/compiler-cli/src/path_mapped_compiler_host.ts b/packages/compiler-cli/src/path_mapped_compiler_host.ts index f17b0a5800..4df2e31740 100644 --- a/packages/compiler-cli/src/path_mapped_compiler_host.ts +++ b/packages/compiler-cli/src/path_mapped_compiler_host.ts @@ -7,12 +7,13 @@ */ import {StaticSymbol} from '@angular/compiler'; -import {AngularCompilerOptions, ModuleMetadata} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; import {CompilerHost, CompilerHostContext} from './compiler_host'; +import {ModuleMetadata} from './metadata/index'; +import {CompilerOptions} from './transformers/api'; const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const DTS = /\.d\.ts$/; @@ -25,7 +26,7 @@ const DTS = /\.d\.ts$/; * loader what to do. */ export class PathMappedCompilerHost extends CompilerHost { - constructor(program: ts.Program, options: AngularCompilerOptions, context: CompilerHostContext) { + constructor(program: ts.Program, options: CompilerOptions, context: CompilerHostContext) { super(program, options, context); } diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 65f4d1560a..7408a0b29b 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -187,7 +187,6 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa return {diagnostics: allDiagnostics, program}; } } - function defaultGatherDiagnostics(program: api.Program): Diagnostics { const allDiagnostics: Diagnostics = []; diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts index e6a2476b3d..e182a4d448 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CollectorOptions, MetadataCollector, MetadataError, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; +import {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '../metadata/index'; export interface LoweringRequest { kind: ts.SyntaxKind; diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 3d686a37f8..17035dff26 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -7,13 +7,13 @@ */ import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, MessageBundle, NgAnalyzedModules, Serializer, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; -import {createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; import {BaseAotCompilerHost} from '../compiler_host'; import {TypeChecker} from '../diagnostics/check_types'; +import {createBundleIndexHost} from '../metadata/index'; import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api'; import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; diff --git a/packages/compiler-cli/test/aot_host_spec.ts b/packages/compiler-cli/test/aot_host_spec.ts index aad08036c3..09983231ef 100644 --- a/packages/compiler-cli/test/aot_host_spec.ts +++ b/packages/compiler-cli/test/aot_host_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ModuleMetadata} from '@angular/tsc-wrapped'; +import {ModuleMetadata} from '@angular/compiler-cli/src/metadata/index'; import * as ts from 'typescript'; import {CompilerHost} from '../src/compiler_host'; diff --git a/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts index 45a9d1a1e9..6d95a5b9df 100644 --- a/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts +++ b/packages/compiler-cli/test/diagnostics/expression_diagnostics_spec.ts @@ -7,10 +7,11 @@ */ import {StaticSymbol} from '@angular/compiler'; -import {AngularCompilerOptions, CompilerHost} from '@angular/compiler-cli'; +import {CompilerHost} from '@angular/compiler-cli'; import * as ts from 'typescript'; import {getExpressionDiagnostics, getTemplateExpressionDiagnostics} from '../../src/diagnostics/expression_diagnostics'; +import {CompilerOptions} from '../../src/transformers/api'; import {Directory} from '../mocks'; import {DiagnosticContext, MockLanguageServiceHost, getDiagnosticTemplateInfo} from './mocks'; @@ -29,7 +30,7 @@ describe('expression diagnostics', () => { service = ts.createLanguageService(host, registry); const program = service.getProgram(); const checker = program.getTypeChecker(); - const options: AngularCompilerOptions = Object.create(host.getCompilationSettings()); + const options: CompilerOptions = Object.create(host.getCompilationSettings()); options.genDir = '/dist'; options.basePath = '/src'; aotHost = new CompilerHost(program, options, host, {verboseInvalidExpression: true}); diff --git a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts b/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts index 317b26ed3e..624b810b30 100644 --- a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts +++ b/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts @@ -7,12 +7,13 @@ */ import {StaticSymbol} from '@angular/compiler'; -import {AngularCompilerOptions, CompilerHost} from '@angular/compiler-cli'; +import {CompilerHost} from '@angular/compiler-cli'; import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, isSource, settings, setup, toMockFileArray} from '@angular/compiler/test/aot/test_util'; import * as ts from 'typescript'; import {Symbol, SymbolQuery, SymbolTable} from '../../src/diagnostics/symbols'; import {getSymbolQuery} from '../../src/diagnostics/typescript_symbols'; +import {CompilerOptions} from '../../src/transformers/api'; import {Directory} from '../mocks'; import {DiagnosticContext, MockLanguageServiceHost} from './mocks'; @@ -40,7 +41,7 @@ describe('symbol query', () => { program = service.getProgram(); checker = program.getTypeChecker(); sourceFile = program.getSourceFile('/quickstart/app/app.component.ts'); - const options: AngularCompilerOptions = Object.create(host.getCompilationSettings()); + const options: CompilerOptions = Object.create(host.getCompilationSettings()); options.genDir = '/dist'; options.basePath = '/quickstart'; const aotHost = new CompilerHost(program, options, host, {verboseInvalidExpression: true}); diff --git a/packages/compiler-cli/test/extract_i18n_spec.ts b/packages/compiler-cli/test/extract_i18n_spec.ts index ff2477b8c8..ffba4b9d5e 100644 --- a/packages/compiler-cli/test/extract_i18n_spec.ts +++ b/packages/compiler-cli/test/extract_i18n_spec.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main} from '../src/extract_i18n'; +import {mainXi18n} from '../src/extract_i18n'; + +import {makeTempDir} from './test_support'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -172,7 +173,7 @@ describe('extract_i18n command line', () => { writeSources(); const exitCode = - main(['-p', basePath, '--i18nFormat=xmb', '--outFile=custom_file.xmb'], errorSpy); + mainXi18n(['-p', basePath, '--i18nFormat=xmb', '--outFile=custom_file.xmb'], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); @@ -186,7 +187,7 @@ describe('extract_i18n command line', () => { writeConfig(); writeSources(); - const exitCode = main(['-p', basePath, '--i18nFormat=xlf', '--locale=fr'], errorSpy); + const exitCode = mainXi18n(['-p', basePath, '--i18nFormat=xlf', '--locale=fr'], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); @@ -201,7 +202,7 @@ describe('extract_i18n command line', () => { writeSources(); const exitCode = - main(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + mainXi18n(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); @@ -216,11 +217,11 @@ describe('extract_i18n command line', () => { writeSources(); const exitCode = - main(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + mainXi18n(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); const moduleOutput = path.join(outDir, 'src', 'module.js'); expect(fs.existsSync(moduleOutput)).toBeFalsy(); }); -}); \ No newline at end of file +}); diff --git a/packages/compiler-cli/test/main_spec.ts b/packages/compiler-cli/test/main_spec.ts deleted file mode 100644 index 1243d05c9c..0000000000 --- a/packages/compiler-cli/test/main_spec.ts +++ /dev/null @@ -1,313 +0,0 @@ -/** - * @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 {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; - -import {main} from '../src/main'; - -function getNgRootDir() { - const moduleFilename = module.filename.replace(/\\/g, '/'); - const distIndex = moduleFilename.indexOf('/dist/all'); - return moduleFilename.substr(0, distIndex); -} - -describe('compiler-cli with disableTransformerPipeline', () => { - let basePath: string; - let outDir: string; - let write: (fileName: string, content: string) => void; - let errorSpy: jasmine.Spy&((s: string) => void); - - function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') { - const json = JSON.parse(tsconfig); - // Note: 'extends' does not work for "angularCompilerOptions" yet. - const ngOptions = json['angularCompilerOptions'] = json['angularCompilerOptions'] || {}; - ngOptions['disableTransformerPipeline'] = true; - write('tsconfig.json', JSON.stringify(json)); - } - - beforeEach(() => { - errorSpy = jasmine.createSpy('consoleError'); - basePath = makeTempDir(); - write = (fileName: string, content: string) => { - fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); - }; - write('tsconfig-base.json', `{ - "compilerOptions": { - "experimentalDecorators": true, - "types": [], - "outDir": "built", - "declaration": true, - "module": "es2015", - "moduleResolution": "node", - "lib": ["es6", "dom"] - } - }`); - outDir = path.resolve(basePath, 'built'); - const ngRootDir = getNgRootDir(); - const nodeModulesPath = path.resolve(basePath, 'node_modules'); - fs.mkdirSync(nodeModulesPath); - fs.symlinkSync( - path.resolve(ngRootDir, 'dist', 'all', '@angular'), - path.resolve(nodeModulesPath, '@angular')); - fs.symlinkSync( - path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs')); - }); - - it('should compile without errors', (done) => { - writeConfig(); - write('test.ts', 'export const A = 1;'); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).not.toHaveBeenCalled(); - expect(exitCode).toEqual(0); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not print the stack trace if user input file does not exist', (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["test.ts"] - }`); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledWith( - `Error File '` + path.join(basePath, 'test.ts') + `' not found.`); - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not print the stack trace if user input file is malformed', (done) => { - writeConfig(); - write('test.ts', 'foo;'); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + `:1:1: Cannot find name 'foo'.`); - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not print the stack trace if cannot find the imported module', (done) => { - writeConfig(); - write('test.ts', `import {MyClass} from './not-exist-deps';`); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + - `:1:23: Cannot find module './not-exist-deps'.`); - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not print the stack trace if cannot import', (done) => { - writeConfig(); - write('empty-deps.ts', 'export const A = 1;'); - write('test.ts', `import {MyClass} from './empty-deps';`); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + `:1:9: Module '"` + - path.join(basePath, 'empty-deps') + `"' has no exported member 'MyClass'.`); - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not print the stack trace if type mismatches', (done) => { - writeConfig(); - write('empty-deps.ts', 'export const A = "abc";'); - write('test.ts', ` - import {A} from './empty-deps'; - A(); - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + - ':3:7: Cannot invoke an expression whose type lacks a call signature. ' + - 'Type \'String\' has no compatible call signatures.'); - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should print the stack trace on compiler internal errors', (done) => { - write('test.ts', 'export const A = 1;'); - - main(['-p', 'not-exist'], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalled(); - expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); - expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); - expect(exitCode).toEqual(2); - done(); - }) - .catch(e => done.fail(e)); - }); - - describe('compile ngfactory files', () => { - it('should only compile ngfactory files that are referenced by root files by default', - (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["mymodule.ts"] - }`); - write('mymodule.ts', ` - import {CommonModule} from '@angular/common'; - import {NgModule} from '@angular/core'; - - @NgModule({ - imports: [CommonModule] - }) - export class MyModule {} - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(exitCode).toEqual(0); - - expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(false); - - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should report errors for ngfactory files that are not referenced by root files', (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["mymodule.ts"], - "angularCompilerOptions": { - "alwaysCompileGeneratedCode": true - } - }`); - write('mymodule.ts', ` - import {NgModule, Component} from '@angular/core'; - - @Component({template: '{{unknownProp}}'}) - export class MyComp {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.calls.mostRecent().args[0]) - .toContain('Error at ' + path.join(basePath, 'mymodule.ngfactory.ts')); - expect(errorSpy.calls.mostRecent().args[0]) - .toContain(`Property 'unknownProp' does not exist on type 'MyComp'`); - - expect(exitCode).toEqual(1); - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should compile ngfactory files that are not referenced by root files', (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["mymodule.ts"], - "angularCompilerOptions": { - "alwaysCompileGeneratedCode": true - } - }`); - write('mymodule.ts', ` - import {CommonModule} from '@angular/common'; - import {NgModule} from '@angular/core'; - - @NgModule({ - imports: [CommonModule] - }) - export class MyModule {} - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(exitCode).toEqual(0); - - expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); - expect(fs.existsSync(path.resolve( - outDir, 'node_modules', '@angular', 'core', 'src', - 'application_module.ngfactory.js'))) - .toBe(true); - - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should not produce ngsummary files by default', (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["mymodule.ts"] - }`); - write('mymodule.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule() - export class MyModule {} - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(exitCode).toEqual(0); - expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngsummary.js'))).toBe(false); - - done(); - }) - .catch(e => done.fail(e)); - }); - - it('should produce ngsummary files if configured', (done) => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "files": ["mymodule.ts"], - "angularCompilerOptions": { - "enableSummariesForJit": true, - "alwaysCompileGeneratedCode": true - } - }`); - write('mymodule.ts', ` - import {NgModule} from '@angular/core'; - - @NgModule() - export class MyModule {} - `); - - main(['-p', basePath], errorSpy) - .then((exitCode) => { - expect(exitCode).toEqual(0); - expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngsummary.js'))).toBe(true); - - done(); - }) - .catch(e => done.fail(e)); - }); - }); -}); diff --git a/packages/compiler-cli/test/metadata/bundler_spec.ts b/packages/compiler-cli/test/metadata/bundler_spec.ts new file mode 100644 index 0000000000..f00a620300 --- /dev/null +++ b/packages/compiler-cli/test/metadata/bundler_spec.ts @@ -0,0 +1,274 @@ +/** + * @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 path from 'path'; +import * as ts from 'typescript'; + +import {MetadataBundler, MetadataBundlerHost} from '../../src/metadata/bundler'; +import {MetadataCollector} from '../../src/metadata/collector'; +import {ClassMetadata, MetadataGlobalReferenceExpression, ModuleMetadata} from '../../src/metadata/schema'; + +import {Directory, open} from './typescript.mocks'; + +describe('metadata bundler', () => { + + it('should be able to bundle a simple library', () => { + const host = new MockStringBundlerHost('/', SIMPLE_LIBRARY); + const bundler = new MetadataBundler('/lib/index', undefined, host); + const result = bundler.getMetadataBundle(); + expect(Object.keys(result.metadata.metadata).sort()).toEqual([ + 'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵa', 'ɵb' + ]); + + const originalOne = './src/one'; + const originalTwo = './src/two/index'; + expect(Object.keys(result.metadata.origins) + .sort() + .map(name => ({name, value: result.metadata.origins ![name]}))) + .toEqual([ + {name: 'ONE_CLASSES', value: originalOne}, {name: 'One', value: originalOne}, + {name: 'OneMore', value: originalOne}, {name: 'TWO_CLASSES', value: originalTwo}, + {name: 'Two', value: originalTwo}, {name: 'TwoMore', value: originalTwo}, + {name: 'ɵa', value: originalOne}, {name: 'ɵb', value: originalTwo} + ]); + expect(result.privates).toEqual([ + {privateName: 'ɵa', name: 'PrivateOne', module: originalOne}, + {privateName: 'ɵb', name: 'PrivateTwo', module: originalTwo} + ]); + }); + + it('should be able to bundle an oddly constructed library', () => { + const host = new MockStringBundlerHost('/', { + 'lib': { + 'index.ts': ` + export * from './src/index'; + `, + 'src': { + 'index.ts': ` + export {One, OneMore, ONE_CLASSES} from './one'; + export {Two, TwoMore, TWO_CLASSES} from './two/index'; + `, + 'one.ts': ` + class One {} + class OneMore extends One {} + class PrivateOne {} + const ONE_CLASSES = [One, OneMore, PrivateOne]; + export {One, OneMore, PrivateOne, ONE_CLASSES}; + `, + 'two': { + 'index.ts': ` + class Two {} + class TwoMore extends Two {} + class PrivateTwo {} + const TWO_CLASSES = [Two, TwoMore, PrivateTwo]; + export {Two, TwoMore, PrivateTwo, TWO_CLASSES}; + ` + } + } + } + }); + const bundler = new MetadataBundler('/lib/index', undefined, host); + const result = bundler.getMetadataBundle(); + expect(Object.keys(result.metadata.metadata).sort()).toEqual([ + 'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵa', 'ɵb' + ]); + expect(result.privates).toEqual([ + {privateName: 'ɵa', name: 'PrivateOne', module: './src/one'}, + {privateName: 'ɵb', name: 'PrivateTwo', module: './src/two/index'} + ]); + }); + + it('should not output windows paths in metadata', () => { + const host = new MockStringBundlerHost('/', { + 'index.ts': ` + export * from './exports/test'; + `, + 'exports': {'test.ts': `export class TestExport {}`} + }); + const bundler = new MetadataBundler('/index', undefined, host); + const result = bundler.getMetadataBundle(); + + expect(result.metadata.origins).toEqual({'TestExport': './exports/test'}); + }); + + it('should convert re-exported to the export', () => { + const host = new MockStringBundlerHost('/', { + 'index.ts': ` + export * from './bar'; + export * from './foo'; + `, + 'bar.ts': ` + import {Foo} from './foo'; + export class Bar extends Foo { + + } + `, + 'foo.ts': ` + export {Foo} from 'foo'; + ` + }); + const bundler = new MetadataBundler('/index', undefined, host); + const result = bundler.getMetadataBundle(); + // Expect the extends reference to refer to the imported module + expect((result.metadata.metadata as any).Bar.extends.module).toEqual('foo'); + expect(result.privates).toEqual([]); + }); + + it('should treat import then export as a simple export', () => { + const host = new MockStringBundlerHost('/', { + 'index.ts': ` + export * from './a'; + export * from './c'; + `, + 'a.ts': ` + import { B } from './b'; + export { B }; + `, + 'b.ts': ` + export class B { } + `, + 'c.ts': ` + import { B } from './b'; + export class C extends B { } + ` + }); + const bundler = new MetadataBundler('/index', undefined, host); + const result = bundler.getMetadataBundle(); + expect(Object.keys(result.metadata.metadata).sort()).toEqual(['B', 'C']); + expect(result.privates).toEqual([]); + }); + + it('should be able to bundle a private from a un-exported module', () => { + const host = new MockStringBundlerHost('/', { + 'index.ts': ` + export * from './foo'; + `, + 'foo.ts': ` + import {Bar} from './bar'; + export class Foo extends Bar { + + } + `, + 'bar.ts': ` + export class Bar {} + ` + }); + const bundler = new MetadataBundler('/index', undefined, host); + const result = bundler.getMetadataBundle(); + expect(Object.keys(result.metadata.metadata).sort()).toEqual(['Foo', 'ɵa']); + expect(result.privates).toEqual([{privateName: 'ɵa', name: 'Bar', module: './bar'}]); + }); + + it('should be able to bundle a library with re-exported symbols', () => { + const host = new MockStringBundlerHost('/', { + 'public-api.ts': ` + export * from './src/core'; + export * from './src/externals'; + `, + 'src': { + 'core.ts': ` + export class A {} + export class B extends A {} + `, + 'externals.ts': ` + export {E, F, G} from 'external_one'; + export * from 'external_two'; + ` + } + }); + + const bundler = new MetadataBundler('/public-api', undefined, host); + const result = bundler.getMetadataBundle(); + expect(result.metadata.exports).toEqual([ + {from: 'external_two'}, { + export: [{name: 'E', as: 'E'}, {name: 'F', as: 'F'}, {name: 'G', as: 'G'}], + from: 'external_one' + } + ]); + expect(result.metadata.origins !['E']).toBeUndefined(); + }); + + it('should be able to de-duplicate symbols of re-exported modules', () => { + const host = new MockStringBundlerHost('/', { + 'public-api.ts': ` + export {A as A2, A, B as B1, B as B2} from './src/core'; + `, + 'src': { + 'core.ts': ` + export class A {} + export class B {} + `, + } + }); + + const bundler = new MetadataBundler('/public-api', undefined, host); + const result = bundler.getMetadataBundle(); + const {A, A2, B1, B2} = result.metadata.metadata as{ + A: ClassMetadata, + A2: MetadataGlobalReferenceExpression, + B1: ClassMetadata, + B2: MetadataGlobalReferenceExpression + }; + expect(A.__symbolic).toEqual('class'); + expect(A2.__symbolic).toEqual('reference'); + expect(A2.name).toEqual('A'); + expect(B1.__symbolic).toEqual('class'); + expect(B2.__symbolic).toEqual('reference'); + expect(B2.name).toEqual('B1'); + }); +}); + +export class MockStringBundlerHost implements MetadataBundlerHost { + collector = new MetadataCollector(); + + constructor(private dirName: string, private directory: Directory) {} + + getMetadataFor(moduleName: string): ModuleMetadata|undefined { + const fileName = path.join(this.dirName, moduleName) + '.ts'; + const text = open(this.directory, fileName); + if (typeof text == 'string') { + const sourceFile = ts.createSourceFile( + fileName, text, ts.ScriptTarget.Latest, /* setParent */ true, ts.ScriptKind.TS); + const diagnostics: ts.Diagnostic[] = (sourceFile as any).parseDiagnostics; + if (diagnostics && diagnostics.length) { + throw Error('Unexpected syntax error in test'); + } + const result = this.collector.getMetadata(sourceFile); + return result; + } + } +} + + +export const SIMPLE_LIBRARY = { + 'lib': { + 'index.ts': ` + export * from './src/index'; + `, + 'src': { + 'index.ts': ` + export {One, OneMore, ONE_CLASSES} from './one'; + export {Two, TwoMore, TWO_CLASSES} from './two/index'; + `, + 'one.ts': ` + export class One {} + export class OneMore extends One {} + export class PrivateOne {} + export const ONE_CLASSES = [One, OneMore, PrivateOne]; + `, + 'two': { + 'index.ts': ` + export class Two {} + export class TwoMore extends Two {} + export class PrivateTwo {} + export const TWO_CLASSES = [Two, TwoMore, PrivateTwo]; + ` + } + } + } +}; diff --git a/packages/compiler-cli/test/metadata/collector_spec.ts b/packages/compiler-cli/test/metadata/collector_spec.ts new file mode 100644 index 0000000000..5282067278 --- /dev/null +++ b/packages/compiler-cli/test/metadata/collector_spec.ts @@ -0,0 +1,1484 @@ +/** + * @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} from '../../src/metadata/collector'; +import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata, isMetadataGlobalReferenceExpression} from '../../src/metadata/schema'; + +import {Directory, Host, expectValidSources} from './typescript.mocks'; + +describe('Collector', () => { + const documentRegistry = ts.createDocumentRegistry(); + let host: Host; + let service: ts.LanguageService; + let program: ts.Program; + let collector: MetadataCollector; + + beforeEach(() => { + host = new Host(FILES, [ + '/app/app.component.ts', + '/app/cases-data.ts', + '/app/error-cases.ts', + '/promise.ts', + '/unsupported-1.ts', + '/unsupported-2.ts', + '/unsupported-3.ts', + 'class-arity.ts', + 'import-star.ts', + 'exported-classes.ts', + 'exported-functions.ts', + 'exported-enum.ts', + 'exported-type.ts', + 'exported-consts.ts', + 'local-symbol-ref.ts', + 'local-function-ref.ts', + 'local-symbol-ref-func.ts', + 'local-symbol-ref-func-dynamic.ts', + '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', + 'static-method-with-if.ts', + 'static-method-with-default.ts', + 'class-inheritance.ts', + 'class-inheritance-parent.ts', + 'class-inheritance-declarations.d.ts', + 'interface-reference.ts' + ]); + service = ts.createLanguageService(host, documentRegistry); + program = service.getProgram(); + collector = new MetadataCollector({quotedNames: true}); + }); + + it('should not have errors in test data', () => { expectValidSources(service, program); }); + + it('should return undefined for modules that have no metadata', () => { + const sourceFile = program.getSourceFile('app/empty.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toBeUndefined(); + }); + + it('should return an interface reference for types', () => { + const sourceFile = program.getSourceFile('/exported-type.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual( + {__symbolic: 'module', version: 3, metadata: {SomeType: {__symbolic: 'interface'}}}); + }); + + it('should return an interface reference for interfaces', () => { + const sourceFile = program.getSourceFile('app/hero.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual( + {__symbolic: 'module', version: 3, metadata: {Hero: {__symbolic: 'interface'}}}); + }); + + it('should be able to collect a simple component\'s metadata', () => { + const sourceFile = program.getSourceFile('app/hero-detail.component.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + HeroDetailComponent: { + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{ + selector: 'my-hero-detail', + template: ` +
+

{{hero.name}} details!

+
{{hero.id}}
+
+ + +
+
+ ` + }] + }], + members: { + hero: [{ + __symbolic: 'property', + decorators: [{ + __symbolic: 'call', + expression: + {__symbolic: 'reference', module: 'angular2/core', name: 'Input'} + }] + }] + } + } + } + }); + }); + + it('should be able to get a more complicated component\'s metadata', () => { + const sourceFile = program.getSourceFile('/app/app.component.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + AppComponent: { + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{ + selector: 'my-app', + template: ` +

My Heroes

+
    +
  • + {{hero.id | lowercase}} {{hero.name | uppercase}} +
  • +
+ + `, + directives: [ + { + __symbolic: 'reference', + module: './hero-detail.component', + name: 'HeroDetailComponent', + }, + {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} + ], + providers: [{__symbolic: 'reference', module: './hero.service', default: true}], + pipes: [ + {__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'}, + {__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'} + ] + }] + }], + members: { + __ctor__: [{ + __symbolic: 'constructor', + parameters: [{__symbolic: 'reference', module: './hero.service', default: true}] + }], + onSelect: [{__symbolic: 'method'}], + ngOnInit: [{__symbolic: 'method'}], + getHeroes: [{__symbolic: 'method'}] + } + } + } + }); + }); + + it('should return the values of exported variables', () => { + const sourceFile = program.getSourceFile('/app/mock-heroes.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + HEROES: [ + {'id': 11, 'name': 'Mr. Nice', '$quoted$': ['id', 'name']}, + {'id': 12, 'name': 'Narco', '$quoted$': ['id', 'name']}, + {'id': 13, 'name': 'Bombasto', '$quoted$': ['id', 'name']}, + {'id': 14, 'name': 'Celeritas', '$quoted$': ['id', 'name']}, + {'id': 15, 'name': 'Magneta', '$quoted$': ['id', 'name']}, + {'id': 16, 'name': 'RubberMan', '$quoted$': ['id', 'name']}, + {'id': 17, 'name': 'Dynama', '$quoted$': ['id', 'name']}, + {'id': 18, 'name': 'Dr IQ', '$quoted$': ['id', 'name']}, + {'id': 19, 'name': 'Magma', '$quoted$': ['id', 'name']}, + {'id': 20, 'name': 'Tornado', '$quoted$': ['id', 'name']} + ] + } + }); + }); + + let casesFile: ts.SourceFile; + let casesMetadata: ModuleMetadata; + + beforeEach(() => { + casesFile = program.getSourceFile('/app/cases-data.ts'); + casesMetadata = collector.getMetadata(casesFile) !; + }); + + it('should provide any reference for an any ctor parameter type', () => { + const casesAny = casesMetadata.metadata['CaseAny']; + expect(casesAny).toBeTruthy(); + const ctorData = casesAny.members !['__ctor__']; + expect(ctorData).toEqual( + [{__symbolic: 'constructor', parameters: [{__symbolic: 'reference', name: 'any'}]}]); + }); + + it('should record annotations on set and get declarations', () => { + const propertyData = { + name: [{ + __symbolic: 'property', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'}, + arguments: ['firstName'] + }] + }] + }; + const caseGetProp = casesMetadata.metadata['GetProp']; + expect(caseGetProp.members).toEqual(propertyData); + const caseSetProp = casesMetadata.metadata['SetProp']; + expect(caseSetProp.members).toEqual(propertyData); + const caseFullProp = casesMetadata.metadata['FullProp']; + expect(caseFullProp.members).toEqual(propertyData); + }); + + it('should record references to parameterized types', () => { + const casesForIn = casesMetadata.metadata['NgFor']; + expect(casesForIn).toEqual({ + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'} + }], + members: { + __ctor__: [{ + __symbolic: 'constructor', + parameters: [{ + __symbolic: 'reference', + name: 'ClassReference', + arguments: [{__symbolic: 'reference', name: 'NgForRow'}] + }] + }] + } + }); + }); + + it('should report errors for destructured imports', () => { + const unsupported1 = program.getSourceFile('/unsupported-1.ts'); + const metadata = collector.getMetadata(unsupported1); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + a: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 16}, + b: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 19}, + c: {__symbolic: 'error', message: 'Destructuring not supported', line: 2, character: 16}, + d: {__symbolic: 'error', message: 'Destructuring not supported', line: 2, character: 19}, + e: {__symbolic: 'error', message: 'Variable not initialized', line: 3, character: 15} + } + }); + }); + + it('should report an error for references to unexpected types', () => { + const unsupported1 = program.getSourceFile('/unsupported-2.ts'); + const metadata = collector.getMetadata(unsupported1) !; + const barClass = metadata.metadata['Bar']; + const ctor = barClass.members !['__ctor__'][0]; + const parameter = ctor.parameters ![0]; + expect(parameter).toEqual({ + __symbolic: 'error', + message: 'Reference to non-exported class', + line: 3, + character: 4, + context: {className: 'Foo'} + }); + }); + + it('should be able to handle import star type references', () => { + const importStar = program.getSourceFile('/import-star.ts'); + const metadata = collector.getMetadata(importStar) !; + const someClass = metadata.metadata['SomeClass']; + const ctor = someClass.members !['__ctor__'][0]; + const parameters = ctor.parameters; + expect(parameters).toEqual([ + {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} + ]); + }); + + it('should record all exported classes', () => { + const sourceFile = program.getSourceFile('/exported-classes.ts'); + const metadata = collector.getMetadata(sourceFile); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + SimpleClass: {__symbolic: 'class'}, + AbstractClass: {__symbolic: 'class'}, + DeclaredClass: {__symbolic: 'class'} + } + }); + }); + + it('should be able to record functions', () => { + const exportedFunctions = program.getSourceFile('/exported-functions.ts'); + const metadata = collector.getMetadata(exportedFunctions); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 3, + metadata: { + one: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: { + a: {__symbolic: 'reference', name: 'a'}, + b: {__symbolic: 'reference', name: 'b'}, + c: {__symbolic: 'reference', name: 'c'} + } + }, + two: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: { + a: {__symbolic: 'reference', name: 'a'}, + b: {__symbolic: 'reference', name: 'b'}, + c: {__symbolic: 'reference', name: 'c'} + } + }, + three: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: [ + {__symbolic: 'reference', name: 'a'}, {__symbolic: 'reference', name: 'b'}, + {__symbolic: 'reference', name: 'c'} + ] + }, + supportsState: { + __symbolic: 'function', + parameters: [], + value: { + __symbolic: 'pre', + operator: '!', + operand: { + __symbolic: 'pre', + operator: '!', + operand: { + __symbolic: 'select', + expression: { + __symbolic: 'select', + expression: {__symbolic: 'reference', name: 'window'}, + member: 'history' + }, + member: 'pushState' + } + } + } + }, + complexFn: {__symbolic: 'function'}, + declaredFn: {__symbolic: 'function'} + } + }); + }); + + it('should be able to handle import star type references', () => { + const importStar = program.getSourceFile('/import-star.ts'); + const metadata = collector.getMetadata(importStar) !; + const someClass = metadata.metadata['SomeClass']; + const ctor = someClass.members !['__ctor__'][0]; + const parameters = ctor.parameters; + expect(parameters).toEqual([ + {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} + ]); + }); + + it('should be able to collect the value of an enum', () => { + const enumSource = program.getSourceFile('/exported-enum.ts'); + const metadata = collector.getMetadata(enumSource) !; + const someEnum: any = metadata.metadata['SomeEnum']; + expect(someEnum).toEqual({A: 0, B: 1, C: 100, D: 101}); + }); + + it('should ignore a non-export enum', () => { + const enumSource = program.getSourceFile('/private-enum.ts'); + const metadata = collector.getMetadata(enumSource) !; + const publicEnum: any = metadata.metadata['PublicEnum']; + const privateEnum: any = metadata.metadata['PrivateEnum']; + expect(publicEnum).toEqual({a: 0, b: 1, c: 2}); + expect(privateEnum).toBeUndefined(); + }); + + it('should be able to collect enums initialized from consts', () => { + const enumSource = program.getSourceFile('/exported-enum.ts'); + const metadata = collector.getMetadata(enumSource) !; + const complexEnum: any = metadata.metadata['ComplexEnum']; + expect(complexEnum).toEqual({ + A: 0, + B: 1, + C: 30, + D: 40, + E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'} + }); + }); + + it('should be able to collect a simple static method', () => { + const staticSource = program.getSourceFile('/static-method.ts'); + const metadata = collector.getMetadata(staticSource) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['MyModule']; + expect(classData).toBeDefined(); + expect(classData.statics).toEqual({ + with: { + __symbolic: 'function', + parameters: ['comp'], + value: [ + {__symbolic: 'reference', name: 'MyModule'}, + {provider: 'a', useValue: {__symbolic: 'reference', name: 'comp'}} + ] + } + }); + }); + + it('should be able to collect a call to a static method', () => { + const staticSource = program.getSourceFile('/static-method-call.ts'); + const metadata = collector.getMetadata(staticSource) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['Foo']; + expect(classData).toBeDefined(); + expect(classData.decorators).toEqual([{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{ + providers: { + __symbolic: 'call', + expression: { + __symbolic: 'select', + expression: {__symbolic: 'reference', module: './static-method', name: 'MyModule'}, + member: 'with' + }, + arguments: ['a'] + } + }] + }]); + }); + + it('should be able to collect a static field', () => { + const staticSource = program.getSourceFile('/static-field.ts'); + const metadata = collector.getMetadata(staticSource) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['MyModule']; + expect(classData).toBeDefined(); + expect(classData.statics).toEqual({VALUE: 'Some string'}); + }); + + it('should be able to collect a reference to a static field', () => { + const staticSource = program.getSourceFile('/static-field-reference.ts'); + const metadata = collector.getMetadata(staticSource) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['Foo']; + expect(classData).toBeDefined(); + expect(classData.decorators).toEqual([{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{ + providers: [{ + provide: 'a', + useValue: { + __symbolic: 'select', + expression: {__symbolic: 'reference', module: './static-field', name: 'MyModule'}, + member: 'VALUE' + } + }] + }] + }]); + }); + + it('should be able to collect a method with a conditional expression', () => { + const source = program.getSourceFile('/static-method-with-if.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['MyModule']; + expect(classData).toBeDefined(); + expect(classData.statics).toEqual({ + with: { + __symbolic: 'function', + parameters: ['cond'], + value: [ + {__symbolic: 'reference', name: 'MyModule'}, { + provider: 'a', + useValue: { + __symbolic: 'if', + condition: {__symbolic: 'reference', name: 'cond'}, + thenExpression: '1', + elseExpression: '2' + } + } + ] + } + }); + }); + + it('should be able to collect a method with a default parameter', () => { + const source = program.getSourceFile('/static-method-with-default.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata).toBeDefined(); + const classData = metadata.metadata['MyModule']; + expect(classData).toBeDefined(); + expect(classData.statics).toEqual({ + with: { + __symbolic: 'function', + parameters: ['comp', 'foo', 'bar'], + defaults: [undefined, true, false], + value: [ + {__symbolic: 'reference', name: 'MyModule'}, { + __symbolic: 'if', + condition: {__symbolic: 'reference', name: 'foo'}, + thenExpression: {provider: 'a', useValue: {__symbolic: 'reference', name: 'comp'}}, + elseExpression: {provider: 'b', useValue: {__symbolic: 'reference', name: 'comp'}} + }, + { + __symbolic: 'if', + condition: {__symbolic: 'reference', name: 'bar'}, + thenExpression: {provider: 'c', useValue: {__symbolic: 'reference', name: 'comp'}}, + elseExpression: {provider: 'd', useValue: {__symbolic: 'reference', name: 'comp'}} + } + ] + } + }); + }); + + it('should be able to collect re-exported symbols', () => { + const source = program.getSourceFile('/re-exports.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata.exports).toEqual([ + {from: './static-field', export: ['MyModule']}, + {from: './static-field-reference', export: [{name: 'Foo', as: 'OtherModule'}]}, + {from: 'angular2/core'} + ]); + }); + + 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'} + }); + }); + + it('should collect an error symbol if collecting a reference to a non-exported symbol', () => { + const source = program.getSourceFile('/local-symbol-ref.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata.metadata).toEqual({ + REQUIRED_VALIDATOR: { + __symbolic: 'error', + message: 'Reference to a local symbol', + line: 3, + character: 8, + context: {name: 'REQUIRED'} + }, + SomeComponent: { + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}] + }] + } + }); + }); + + it('should collect an error symbol if collecting a reference to a non-exported function', () => { + const source = program.getSourceFile('/local-function-ref.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata.metadata).toEqual({ + REQUIRED_VALIDATOR: { + __symbolic: 'error', + message: 'Reference to a non-exported function', + line: 3, + character: 13, + context: {name: 'required'} + }, + SomeComponent: { + __symbolic: 'class', + decorators: [{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, + arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}] + }] + } + }); + }); + + it('should collect an error for a simple function that references a local variable', () => { + const source = program.getSourceFile('/local-symbol-ref-func.ts'); + const metadata = collector.getMetadata(source) !; + expect(metadata.metadata).toEqual({ + foo: { + __symbolic: 'function', + parameters: ['index'], + value: { + __symbolic: 'error', + message: 'Reference to a local symbol', + line: 1, + character: 8, + context: {name: 'localSymbol'} + } + } + }); + }); + + it('should collect any for interface parameter reference', () => { + const source = program.getSourceFile('/interface-reference.ts'); + const metadata = collector.getMetadata(source) !; + expect((metadata.metadata['SomeClass'] as ClassMetadata).members).toEqual({ + __ctor__: [{ + __symbolic: 'constructor', + parameterDecorators: [[{ + __symbolic: 'call', + expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Inject'}, + arguments: ['a'] + }]], + parameters: [{__symbolic: 'reference', name: 'any'}] + }] + }); + }); + + describe('with interpolations', () => { + function e(expr: string, prefix?: string) { + const metadata = collectSource(`${prefix || ''} export let value = ${expr};`); + return expect(metadata.metadata['value']); + } + + it('should be able to collect a raw interpolated string', + () => { e('`simple value`').toBe('simple value'); }); + + it('should be able to interpolate a single value', + () => { e('`${foo}`', 'const foo = "foo value"').toBe('foo value'); }); + + it('should be able to interpolate multiple values', () => { + e('`foo:${foo}, bar:${bar}, end`', 'const foo = "foo"; const bar = "bar";') + .toBe('foo:foo, bar:bar, end'); + }); + + it('should be able to interpolate with an imported reference', () => { + e('`external:${external}`', 'import {external} from "./external";').toEqual({ + __symbolic: 'binop', + operator: '+', + left: 'external:', + right: { + __symbolic: 'reference', + module: './external', + name: 'external', + } + }); + }); + + it('should simplify a redundant template', () => { + e('`${external}`', 'import {external} from "./external";') + .toEqual({__symbolic: 'reference', module: './external', name: 'external'}); + }); + + it('should be able to collect complex template with imported references', () => { + e('`foo:${foo}, bar:${bar}, end`', 'import {foo, bar} from "./external";').toEqual({ + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: 'foo:', + right: {__symbolic: 'reference', module: './external', name: 'foo'} + }, + right: ', bar:' + }, + right: {__symbolic: 'reference', module: './external', name: 'bar'} + }, + right: ', end' + }); + }); + + it('should reject a tagged literal', () => { + e('tag`some value`').toEqual({ + __symbolic: 'error', + message: 'Tagged template expressions are not supported in metadata', + line: 0, + character: 20 + }); + }); + }); + + it('should ignore |null or |undefined in type expressions', () => { + const metadata = collectSource(` + import {Foo} from './foo'; + export class SomeClass { + constructor (a: Foo, b: Foo | null, c: Foo | undefined, d: Foo | undefined | null, e: Foo | undefined | null | Foo) {} + } + `); + expect((metadata.metadata['SomeClass'] as ClassMetadata).members).toEqual({ + __ctor__: [{ + __symbolic: 'constructor', + parameters: [ + {__symbolic: 'reference', module: './foo', name: 'Foo'}, + {__symbolic: 'reference', module: './foo', name: 'Foo'}, + {__symbolic: 'reference', module: './foo', name: 'Foo'}, + {__symbolic: 'reference', module: './foo', name: 'Foo'}, + {__symbolic: 'reference', module: './foo', name: 'Foo'} + ] + }] + }); + }); + + it('should treat exported class expressions as a class', () => { + const source = ts.createSourceFile( + '', ` + export const InjectionToken: {new(desc: string): InjectionToken;} = class { + constructor(protected _desc: string) {} + + toString(): string { return \`InjectionToken ${this._desc}\`; } + } as any;`, + ts.ScriptTarget.Latest, true); + const metadata = collector.getMetadata(source) !; + expect(metadata.metadata).toEqual({InjectionToken: {__symbolic: 'class'}}); + }); + + describe('in strict mode', () => { + it('should throw if an error symbol is collecting a reference to a non-exported symbol', () => { + const source = program.getSourceFile('/local-symbol-ref.ts'); + expect(() => collector.getMetadata(source, true)).toThrowError(/Reference to a local symbol/); + }); + + it('should throw if an error if collecting a reference to a non-exported function', () => { + const source = program.getSourceFile('/local-function-ref.ts'); + expect(() => collector.getMetadata(source, true)) + .toThrowError(/Reference to a non-exported function/); + }); + + it('should throw for references to unexpected types', () => { + const unsupported2 = program.getSourceFile('/unsupported-2.ts'); + expect(() => collector.getMetadata(unsupported2, true)) + .toThrowError(/Reference to non-exported class/); + }); + + it('should throw for errors in a static method', () => { + const unsupported3 = program.getSourceFile('/unsupported-3.ts'); + expect(() => collector.getMetadata(unsupported3, true)) + .toThrowError(/Reference to a non-exported class/); + }); + }); + + describe('with invalid input', () => { + it('should not throw with a class with no name', () => { + const fileName = '/invalid-class.ts'; + override(fileName, 'export class'); + const invalidClass = program.getSourceFile(fileName); + expect(() => collector.getMetadata(invalidClass)).not.toThrow(); + }); + + it('should not throw with a function with no name', () => { + const fileName = '/invalid-function.ts'; + override(fileName, 'export function'); + const invalidFunction = program.getSourceFile(fileName); + expect(() => collector.getMetadata(invalidFunction)).not.toThrow(); + }); + }); + + describe('inheritance', () => { + it('should record `extends` clauses for declared classes', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')) !; + expect(metadata.metadata['DeclaredChildClass']) + .toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}}); + }); + + it('should record `extends` clauses for classes in the same file', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')) !; + expect(metadata.metadata['ChildClassSameFile']) + .toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}}); + }); + + it('should record `extends` clauses for classes in a different file', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts')) !; + expect(metadata.metadata['ChildClassOtherFile']).toEqual({ + __symbolic: 'class', + extends: { + __symbolic: 'reference', + module: './class-inheritance-parent', + name: 'ParentClassFromOtherFile' + } + }); + }); + + function expectClass(entry: MetadataEntry): entry is ClassMetadata { + const result = isClassMetadata(entry); + expect(result).toBeTruthy(); + return result; + } + + it('should collect the correct arity for a class', () => { + const metadata = collector.getMetadata(program.getSourceFile('/class-arity.ts')) !; + + const zero = metadata.metadata['Zero']; + if (expectClass(zero)) expect(zero.arity).toBeUndefined(); + const one = metadata.metadata['One']; + if (expectClass(one)) expect(one.arity).toBe(1); + const two = metadata.metadata['Two']; + if (expectClass(two)) expect(two.arity).toBe(2); + const three = metadata.metadata['Three']; + if (expectClass(three)) expect(three.arity).toBe(3); + const nine = metadata.metadata['Nine']; + if (expectClass(nine)) expect(nine.arity).toBe(9); + }); + }); + + describe('regerssion', () => { + it('should be able to collect a short-hand property value', () => { + const metadata = collectSource(` + const children = { f1: 1 }; + export const r = [ + {path: ':locale', children} + ]; + `); + expect(metadata.metadata).toEqual({r: [{path: ':locale', children: {f1: 1}}]}); + }); + + // #17518 + it('should skip a default function', () => { + const metadata = collectSource(` + export default function () { + + const mainRoutes = [ + {name: 'a', abstract: true, component: 'main'}, + + {name: 'a.welcome', url: '/welcome', component: 'welcome'} + ]; + + return mainRoutes; + + }`); + expect(metadata).toBeUndefined(); + }); + + it('should skip a named default export', () => { + const metadata = collectSource(` + function mainRoutes() { + + const mainRoutes = [ + {name: 'a', abstract: true, component: 'main'}, + + {name: 'a.welcome', url: '/welcome', component: 'welcome'} + ]; + + return mainRoutes; + + } + + exports = foo; + `); + expect(metadata).toBeUndefined(); + }); + + it('should be able to collect an invalid access expression', () => { + const source = createSource(` + import {Component} from '@angular/core'; + + const value = []; + @Component({ + provider: [{provide: 'some token', useValue: value[]}] + }) + export class MyComponent {} + `); + const metadata = collector.getMetadata(source) !; + expect(metadata.metadata.MyComponent).toEqual({ + __symbolic: 'class', + decorators: [{ + __symbolic: 'error', + message: 'Expression form not supported', + line: 5, + character: 55 + }] + }); + }); + }); + + describe('references', () => { + beforeEach(() => { collector = new MetadataCollector({quotedNames: true}); }); + + it('should record a reference to an exported field of a useValue', () => { + const metadata = collectSource(` + export var someValue = 1; + export const v = { + useValue: someValue + }; + `); + expect(metadata.metadata['someValue']).toEqual(1); + expect(metadata.metadata['v']).toEqual({ + useValue: {__symbolic: 'reference', name: 'someValue'} + }); + }); + + it('should leave external references in place in an object literal', () => { + const metadata = collectSource(` + export const myLambda = () => [1, 2, 3]; + const indirect = [{a: 1, b: 3: c: myLambda}]; + export const v = { + v: {i: indirect} + } + `); + expect(metadata.metadata['v']).toEqual({ + v: {i: [{a: 1, b: 3, c: {__symbolic: 'reference', name: 'myLambda'}}]} + }); + }); + + it('should leave an external reference in place in an array literal', () => { + const metadata = collectSource(` + export const myLambda = () => [1, 2, 3]; + const indirect = [1, 3, myLambda}]; + export const v = { + v: {i: indirect} + } + `); + expect(metadata.metadata['v']).toEqual({ + v: {i: [1, 3, {__symbolic: 'reference', name: 'myLambda'}]} + }); + }); + }); + + describe('substitutions', () => { + const lambdaTemp = 'lambdaTemp'; + + it('should be able to substitute a lambda', () => { + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should compose substitution functions', () => { + const collector = new MetadataCollector({ + substituteExpression: (value, node) => isMetadataGlobalReferenceExpression(value) && + value.name == lambdaTemp ? + {__symbolic: 'reference', name: value.name + '2'} : + value + }); + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp + '2'}); + }); + }); + + function override(fileName: string, content: string) { + host.overrideFile(fileName, content); + host.addFile(fileName); + program = service.getProgram(); + } + + function collectSource(content: string): ModuleMetadata { + const sourceFile = createSource(content); + return collector.getMetadata(sourceFile) !; + } +}); + +// TODO: Do not use \` in a template literal as it confuses clang-format +const FILES: Directory = { + 'app': { + 'app.component.ts': ` + import {Component as MyComponent, OnInit} from 'angular2/core'; + import * as common from 'angular2/common'; + import {Hero} from './hero'; + import {HeroDetailComponent} from './hero-detail.component'; + import HeroService from './hero.service'; + // thrown away + import 'angular2/core'; + + @MyComponent({ + selector: 'my-app', + template:` + + '`' + + ` +

My Heroes

+
    +
  • + {{hero.id | lowercase}} {{hero.name | uppercase}} +
  • +
+ + ` + + '`' + + `, + directives: [HeroDetailComponent, common.NgFor], + providers: [HeroService], + pipes: [common.LowerCasePipe, common.UpperCasePipe] + }) + export class AppComponent implements OnInit { + public title = 'Tour of Heroes'; + public heroes: Hero[]; + public selectedHero: Hero; + + constructor(private _heroService: HeroService) { } + + onSelect(hero: Hero) { this.selectedHero = hero; } + + ngOnInit() { + this.getHeroes() + } + + getHeroes() { + this._heroService.getHeroesSlowly().then(heroes => this.heroes = heroes); + } + }`, + 'hero.ts': ` + export interface Hero { + id: number; + name: string; + }`, + 'empty.ts': ``, + 'hero-detail.component.ts': ` + import {Component, Input} from 'angular2/core'; + import {Hero} from './hero'; + + @Component({ + selector: 'my-hero-detail', + template: ` + + '`' + + ` +
+

{{hero.name}} details!

+
{{hero.id}}
+
+ + +
+
+ ` + + '`' + + `, + }) + export class HeroDetailComponent { + @Input() public hero: Hero; + }`, + 'mock-heroes.ts': ` + import {Hero as Hero} from './hero'; + + export const HEROES: Hero[] = [ + {"id": 11, "name": "Mr. Nice"}, + {"id": 12, "name": "Narco"}, + {"id": 13, "name": "Bombasto"}, + {"id": 14, "name": "Celeritas"}, + {"id": 15, "name": "Magneta"}, + {"id": 16, "name": "RubberMan"}, + {"id": 17, "name": "Dynama"}, + {"id": 18, "name": "Dr IQ"}, + {"id": 19, "name": "Magma"}, + {"id": 20, "name": "Tornado"} + ];`, + 'default-exporter.ts': ` + let a: string; + export default a; + `, + 'hero.service.ts': ` + import {Injectable} from 'angular2/core'; + import {HEROES} from './mock-heroes'; + import {Hero} from './hero'; + + @Injectable() + class HeroService { + getHeros() { + return Promise.resolve(HEROES); + } + + getHeroesSlowly() { + return new Promise(resolve => + setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds + } + } + export default HeroService;`, + 'cases-data.ts': ` + import {Injectable, Input} from 'angular2/core'; + + @Injectable() + export class CaseAny { + constructor(param: any) {} + } + + @Injectable() + export class GetProp { + private _name: string; + @Input('firstName') get name(): string { + return this._name; + } + } + + @Injectable() + export class SetProp { + private _name: string; + @Input('firstName') set name(value: string) { + this._name = value; + } + } + + @Injectable() + export class FullProp { + private _name: string; + @Input('firstName') get name(): string { + return this._name; + } + set name(value: string) { + this._name = value; + } + } + + export class ClassReference { } + export class NgForRow { + + } + + @Injectable() + export class NgFor { + constructor (public ref: ClassReference) {} + } + `, + 'error-cases.ts': ` + import HeroService from './hero.service'; + + export class CaseCtor { + constructor(private _heroService: HeroService) { } + } + ` + }, + 'promise.ts': ` + interface PromiseLike { + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): PromiseLike; + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): PromiseLike; + } + + interface Promise { + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): Promise; + then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): Promise; + catch(onrejected?: (reason: any) => T | PromiseLike): Promise; + catch(onrejected?: (reason: any) => void): Promise; + } + + interface PromiseConstructor { + prototype: Promise; + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; + reject(reason: any): Promise; + reject(reason: any): Promise; + resolve(value: T | PromiseLike): Promise; + resolve(): Promise; + } + + declare var Promise: PromiseConstructor; + `, + 'class-arity.ts': ` + export class Zero {} + export class One {} + export class Two {} + export class Three {} + export class Nine {} + `, + 'unsupported-1.ts': ` + export let {a, b} = {a: 1, b: 2}; + export let [c, d] = [1, 2]; + export let e; + `, + 'unsupported-2.ts': ` + import {Injectable} from 'angular2/core'; + + class Foo {} + + @Injectable() + export class Bar { + constructor(private f: Foo) {} + } + `, + 'unsupported-3.ts': ` + class Foo {} + + export class SomeClass { + static someStatic() { + return Foo; + } + } + `, + 'interface-reference.ts': ` + import {Injectable, Inject} from 'angular2/core'; + export interface Test {} + + @Injectable() + export class SomeClass { + constructor(@Inject("a") test: Test) {} + } + `, + 'import-star.ts': ` + import {Injectable} from 'angular2/core'; + import * as common from 'angular2/common'; + + @Injectable() + export class SomeClass { + constructor(private f: common.NgFor) {} + } + `, + 'exported-classes.ts': ` + export class SimpleClass {} + export abstract class AbstractClass {} + export declare class DeclaredClass {} + `, + 'class-inheritance-parent.ts': ` + export class ParentClassFromOtherFile {} + `, + 'class-inheritance.ts': ` + import {ParentClassFromOtherFile} from './class-inheritance-parent'; + + export class ParentClass {} + + export declare class DeclaredChildClass extends ParentClass {} + + export class ChildClassSameFile extends ParentClass {} + + export class ChildClassOtherFile extends ParentClassFromOtherFile {} + `, + 'exported-functions.ts': ` + export function one(a: string, b: string, c: string) { + return {a: a, b: b, c: c}; + } + export function two(a: string, b: string, c: string) { + return {a, b, c}; + } + export function three({a, b, c}: {a: string, b: string, c: string}) { + return [a, b, c]; + } + export function supportsState(): boolean { + return !!window.history.pushState; + } + export function complexFn(x: any): boolean { + if (x) { + return true; + } else { + return false; + } + } + export declare function declaredFn(); + `, + 'exported-type.ts': ` + export type SomeType = 'a' | 'b'; + `, + 'exported-enum.ts': ` + import {constValue} from './exported-consts'; + + export const someValue = 30; + export enum SomeEnum { A, B, C = 100, D }; + export enum ComplexEnum { A, B, C = someValue, D = someValue + 10, E = constValue }; + `, + 'exported-consts.ts': ` + export const constValue = 100; + `, + 'static-method.ts': ` + export class MyModule { + static with(comp: any): any[] { + return [ + MyModule, + { provider: 'a', useValue: comp } + ]; + } + } + `, + 'static-method-with-default.ts': ` + export class MyModule { + static with(comp: any, foo: boolean = true, bar: boolean = false): any[] { + return [ + MyModule, + foo ? { provider: 'a', useValue: comp } : {provider: 'b', useValue: comp}, + bar ? { provider: 'c', useValue: comp } : {provider: 'd', useValue: comp} + ]; + } + } + `, + 'static-method-call.ts': ` + import {Component} from 'angular2/core'; + import {MyModule} from './static-method'; + + @Component({ + providers: MyModule.with('a') + }) + export class Foo { } + `, + 'static-field.ts': ` + export class MyModule { + static VALUE = 'Some string'; + } + `, + 'static-field-reference.ts': ` + import {Component} from 'angular2/core'; + import {MyModule} from './static-field'; + + @Component({ + providers: [ { provide: 'a', useValue: MyModule.VALUE } ] + }) + export class Foo { } + `, + 'static-method-with-if.ts': ` + export class MyModule { + static with(cond: boolean): any[] { + return [ + MyModule, + { provider: 'a', useValue: cond ? '1' : '2' } + ]; + } + } + `, + 're-exports.ts': ` + export {MyModule} from './static-field'; + export {Foo as OtherModule} from './static-field-reference'; + export * from 'angular2/core'; + `, + 're-exports-2.ts': ` + import {MyModule} from './static-field'; + import {Foo as OtherModule} from './static-field-reference'; + 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'; + + var REQUIRED; + + export const REQUIRED_VALIDATOR: any = { + provide: 'SomeToken', + useValue: REQUIRED, + multi: true + }; + + @Component({ + providers: [REQUIRED_VALIDATOR] + }) + export class SomeComponent {} + `, + 'private-enum.ts': ` + export enum PublicEnum { a, b, c } + enum PrivateEnum { e, f, g } + `, + 'local-function-ref.ts': ` + import {Component, Validators} from 'angular2/core'; + + function required() {} + + export const REQUIRED_VALIDATOR: any = { + provide: 'SomeToken', + useValue: required, + multi: true + }; + + @Component({ + providers: [REQUIRED_VALIDATOR] + }) + export class SomeComponent {} + `, + 'local-symbol-ref-func.ts': ` + var localSymbol: any[]; + + export function foo(index: number): string { + return localSymbol[index]; + } + `, + 'node_modules': { + 'angular2': { + 'core.d.ts': ` + export interface Type extends Function { } + export interface TypeDecorator { + (type: T): T; + (target: Object, propertyKey?: string | symbol, parameterIndex?: number): void; + annotations: any[]; + } + export interface ComponentDecorator extends TypeDecorator { } + export interface ComponentFactory { + (obj: { + selector?: string; + inputs?: string[]; + outputs?: string[]; + properties?: string[]; + events?: string[]; + host?: { + [key: string]: string; + }; + bindings?: any[]; + providers?: any[]; + exportAs?: string; + moduleId?: string; + queries?: { + [key: string]: any; + }; + viewBindings?: any[]; + viewProviders?: any[]; + templateUrl?: string; + template?: string; + styleUrls?: string[]; + styles?: string[]; + directives?: Array; + pipes?: Array; + }): ComponentDecorator; + } + export declare var Component: ComponentFactory; + export interface InputFactory { + (bindingPropertyName?: string): any; + new (bindingPropertyName?: string): any; + } + export declare var Input: InputFactory; + export interface InjectableFactory { + (): any; + } + export declare var Injectable: InjectableFactory; + export interface InjectFactory { + (binding?: any): any; + new (binding?: any): any; + } + export declare var Inject: InjectFactory; + export interface OnInit { + ngOnInit(): any; + } + export class Validators { + static required(): void; + } + `, + 'common.d.ts': ` + export declare class NgFor { + ngForOf: any; + ngForTemplate: any; + ngDoCheck(): void; + } + export declare class LowerCasePipe { + transform(value: string, args?: any[]): string; + } + export declare class UpperCasePipe { + transform(value: string, args?: any[]): string; + } + ` + } + } +}; + +function createSource(text: string): ts.SourceFile { + return ts.createSourceFile('', text, ts.ScriptTarget.Latest, true); +} diff --git a/packages/compiler-cli/test/metadata/evaluator_spec.ts b/packages/compiler-cli/test/metadata/evaluator_spec.ts new file mode 100644 index 0000000000..d097e1955d --- /dev/null +++ b/packages/compiler-cli/test/metadata/evaluator_spec.ts @@ -0,0 +1,373 @@ +/** + * @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 fs from 'fs'; +import * as ts from 'typescript'; + +import {Evaluator} from '../../src/metadata/evaluator'; +import {Symbols} from '../../src/metadata/symbols'; + +import {Directory, Host, expectNoDiagnostics, findVar, findVarInitializer} from './typescript.mocks'; + +describe('Evaluator', () => { + const documentRegistry = ts.createDocumentRegistry(); + let host: ts.LanguageServiceHost; + let service: ts.LanguageService; + let program: ts.Program; + let typeChecker: ts.TypeChecker; + let symbols: Symbols; + let evaluator: Evaluator; + + beforeEach(() => { + host = new Host(FILES, [ + 'expressions.ts', 'consts.ts', 'const_expr.ts', 'forwardRef.ts', 'classes.ts', + 'newExpression.ts', 'errors.ts', 'declared.ts' + ]); + service = ts.createLanguageService(host, documentRegistry); + program = service.getProgram(); + typeChecker = program.getTypeChecker(); + symbols = new Symbols(null as any as ts.SourceFile); + evaluator = new Evaluator(symbols, new Map()); + }); + + it('should not have typescript errors in test data', () => { + expectNoDiagnostics(service.getCompilerOptionsDiagnostics()); + for (const sourceFile of program.getSourceFiles()) { + expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName)); + if (sourceFile.fileName != 'errors.ts') { + // Skip errors.ts because we it has intentional semantic errors that we are testing for. + expectNoDiagnostics(service.getSemanticDiagnostics(sourceFile.fileName)); + } + } + }); + + it('should be able to fold literal expressions', () => { + const consts = program.getSourceFile('consts.ts'); + expect(evaluator.isFoldable(findVarInitializer(consts, 'someName'))).toBeTruthy(); + expect(evaluator.isFoldable(findVarInitializer(consts, 'someBool'))).toBeTruthy(); + expect(evaluator.isFoldable(findVarInitializer(consts, 'one'))).toBeTruthy(); + expect(evaluator.isFoldable(findVarInitializer(consts, 'two'))).toBeTruthy(); + }); + + it('should be able to fold expressions with foldable references', () => { + const expressions = program.getSourceFile('expressions.ts'); + symbols.define('someName', 'some-name'); + symbols.define('someBool', true); + symbols.define('one', 1); + symbols.define('two', 2); + expect(evaluator.isFoldable(findVarInitializer(expressions, 'three'))).toBeTruthy(); + expect(evaluator.isFoldable(findVarInitializer(expressions, 'four'))).toBeTruthy(); + symbols.define('three', 3); + symbols.define('four', 4); + expect(evaluator.isFoldable(findVarInitializer(expressions, 'obj'))).toBeTruthy(); + expect(evaluator.isFoldable(findVarInitializer(expressions, 'arr'))).toBeTruthy(); + }); + + it('should be able to evaluate literal expressions', () => { + const consts = program.getSourceFile('consts.ts'); + expect(evaluator.evaluateNode(findVarInitializer(consts, 'someName'))).toBe('some-name'); + expect(evaluator.evaluateNode(findVarInitializer(consts, 'someBool'))).toBe(true); + expect(evaluator.evaluateNode(findVarInitializer(consts, 'one'))).toBe(1); + expect(evaluator.evaluateNode(findVarInitializer(consts, 'two'))).toBe(2); + }); + + it('should be able to evaluate expressions', () => { + const expressions = program.getSourceFile('expressions.ts'); + symbols.define('someName', 'some-name'); + symbols.define('someBool', true); + symbols.define('one', 1); + symbols.define('two', 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'three'))).toBe(3); + symbols.define('three', 3); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'four'))).toBe(4); + symbols.define('four', 4); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'obj'))) + .toEqual({one: 1, two: 2, three: 3, four: 4}); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'arr'))).toEqual([1, 2, 3, 4]); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bTrue'))).toEqual(true); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bFalse'))).toEqual(false); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bAnd'))).toEqual(true); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bOr'))).toEqual(true); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'nDiv'))).toEqual(2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'nMod'))).toEqual(1); + + + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bLOr'))).toEqual(false || true); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bLAnd'))).toEqual(true && true); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bBOr'))).toEqual(0x11 | 0x22); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bBAnd'))).toEqual(0x11 & 0x03); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bXor'))).toEqual(0x11 ^ 0x21); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bEqual'))) + .toEqual(1 == '1'); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bNotEqual'))) + .toEqual(1 != '1'); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bIdentical'))) + .toEqual(1 === '1'); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bNotIdentical'))) + .toEqual(1 !== '1'); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bLessThan'))).toEqual(1 < 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bGreaterThan'))).toEqual(1 > 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bLessThanEqual'))) + .toEqual(1 <= 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bGreaterThanEqual'))) + .toEqual(1 >= 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bShiftLeft'))).toEqual(1 << 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bShiftRight'))).toEqual(-1 >> 2); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'bShiftRightU'))) + .toEqual(-1 >>> 2); + + }); + + it('should report recursive references as symbolic', () => { + const expressions = program.getSourceFile('expressions.ts'); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'recursiveA'))) + .toEqual({__symbolic: 'reference', name: 'recursiveB'}); + expect(evaluator.evaluateNode(findVarInitializer(expressions, 'recursiveB'))) + .toEqual({__symbolic: 'reference', name: 'recursiveA'}); + }); + + it('should correctly handle special cases for CONST_EXPR', () => { + const const_expr = program.getSourceFile('const_expr.ts'); + expect(evaluator.evaluateNode(findVarInitializer(const_expr, 'bTrue'))).toEqual(true); + expect(evaluator.evaluateNode(findVarInitializer(const_expr, 'bFalse'))).toEqual(false); + }); + + it('should resolve a forwardRef', () => { + const forwardRef = program.getSourceFile('forwardRef.ts'); + expect(evaluator.evaluateNode(findVarInitializer(forwardRef, 'bTrue'))).toEqual(true); + expect(evaluator.evaluateNode(findVarInitializer(forwardRef, 'bFalse'))).toEqual(false); + }); + + it('should return new expressions', () => { + symbols.define('Value', {__symbolic: 'reference', module: './classes', name: 'Value'}); + evaluator = new Evaluator(symbols, new Map()); + const newExpression = program.getSourceFile('newExpression.ts'); + expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'someValue'))).toEqual({ + __symbolic: 'new', + expression: {__symbolic: 'reference', name: 'Value', module: './classes'}, + arguments: ['name', 12] + }); + expect(evaluator.evaluateNode(findVarInitializer(newExpression, 'complex'))).toEqual({ + __symbolic: 'new', + expression: {__symbolic: 'reference', name: 'Value', module: './classes'}, + arguments: ['name', 12] + }); + }); + + it('should support referene to a declared module type', () => { + const declared = program.getSourceFile('declared.ts'); + const aDecl = findVar(declared, 'a') !; + expect(evaluator.evaluateNode(aDecl.type !)).toEqual({ + __symbolic: 'select', + expression: {__symbolic: 'reference', name: 'Foo'}, + member: 'A' + }); + }); + + it('should return errors for unsupported expressions', () => { + const errors = program.getSourceFile('errors.ts'); + const fDecl = findVar(errors, 'f') !; + expect(evaluator.evaluateNode(fDecl.initializer !)) + .toEqual( + {__symbolic: 'error', message: 'Function call not supported', line: 1, character: 12}); + const eDecl = findVar(errors, 'e') !; + expect(evaluator.evaluateNode(eDecl.type !)).toEqual({ + __symbolic: 'error', + message: 'Could not resolve type', + line: 2, + character: 11, + context: {typeName: 'NotFound'} + }); + const sDecl = findVar(errors, 's') !; + expect(evaluator.evaluateNode(sDecl.initializer !)).toEqual({ + __symbolic: 'error', + message: 'Name expected', + line: 3, + character: 14, + context: {received: '1'} + }); + const tDecl = findVar(errors, 't') !; + expect(evaluator.evaluateNode(tDecl.initializer !)).toEqual({ + __symbolic: 'error', + message: 'Expression form not supported', + line: 4, + character: 12 + }); + }); + + it('should be able to fold an array spread', () => { + const expressions = program.getSourceFile('expressions.ts'); + symbols.define('arr', [1, 2, 3, 4]); + const arrSpread = findVar(expressions, 'arrSpread') !; + expect(evaluator.evaluateNode(arrSpread.initializer !)).toEqual([0, 1, 2, 3, 4, 5]); + }); + + it('should be able to produce a spread expression', () => { + const expressions = program.getSourceFile('expressions.ts'); + const arrSpreadRef = findVar(expressions, 'arrSpreadRef') !; + expect(evaluator.evaluateNode(arrSpreadRef.initializer !)).toEqual([ + 0, {__symbolic: 'spread', expression: {__symbolic: 'reference', name: 'arrImport'}}, 5 + ]); + }); + + it('should be able to handle a new expression with no arguments', () => { + const source = sourceFileOf(` + export var a = new f; + `); + const expr = findVar(source, 'a') !; + expect(evaluator.evaluateNode(expr.initializer !)) + .toEqual({__symbolic: 'new', expression: {__symbolic: 'reference', name: 'f'}}); + }); + + describe('with substitution', () => { + let evaluator: Evaluator; + const lambdaTemp = 'lambdaTemp'; + + beforeEach(() => { + evaluator = new Evaluator(symbols, new Map(), { + substituteExpression: (value, node) => { + if (node.kind == ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + } + }); + }); + + it('should be able to substitute a lambda with a reference', () => { + const source = sourceFileOf(` + var b = 1; + export var a = () => b; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)) + .toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should be able to substitute a lambda in an expression', () => { + const source = sourceFileOf(` + var b = 1; + export var a = [ + { provide: 'someValue': useFactory: () => b } + ]; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)).toEqual([ + {provide: 'someValue', useFactory: {__symbolic: 'reference', name: lambdaTemp}} + ]); + }); + }); +}); + +function sourceFileOf(text: string): ts.SourceFile { + return ts.createSourceFile('test.ts', text, ts.ScriptTarget.Latest, true); +} + +const FILES: Directory = { + 'directives.ts': ` + export function Pipe(options: { name?: string, pure?: boolean}) { + return function(fn: Function) { } + } + `, + 'classes.ts': ` + export class Value { + constructor(public name: string, public value: any) {} + } + `, + 'consts.ts': ` + export var someName = 'some-name'; + export var someBool = true; + export var one = 1; + export var two = 2; + export var arrImport = [1, 2, 3, 4]; + `, + 'expressions.ts': ` + import {arrImport} from './consts'; + + export var someName = 'some-name'; + export var someBool = true; + export var one = 1; + export var two = 2; + + export var three = one + two; + export var four = two * two; + export var obj = { one: one, two: two, three: three, four: four }; + export var arr = [one, two, three, four]; + export var bTrue = someBool; + export var bFalse = !someBool; + export var bAnd = someBool && someBool; + export var bOr = someBool || someBool; + export var nDiv = four / two; + export var nMod = (four + one) % two; + + export var bLOr = false || true; // true + export var bLAnd = true && true; // true + export var bBOr = 0x11 | 0x22; // 0x33 + export var bBAnd = 0x11 & 0x03; // 0x01 + export var bXor = 0x11 ^ 0x21; // 0x20 + export var bEqual = 1 == "1"; // true + export var bNotEqual = 1 != "1"; // false + export var bIdentical = 1 === "1"; // false + export var bNotIdentical = 1 !== "1"; // true + export var bLessThan = 1 < 2; // true + export var bGreaterThan = 1 > 2; // false + export var bLessThanEqual = 1 <= 2; // true + export var bGreaterThanEqual = 1 >= 2; // false + export var bShiftLeft = 1 << 2; // 0x04 + export var bShiftRight = -1 >> 2; // -1 + export var bShiftRightU = -1 >>> 2; // 0x3fffffff + + export var arrSpread = [0, ...arr, 5]; + + export var arrSpreadRef = [0, ...arrImport, 5]; + + export var recursiveA = recursiveB; + export var recursiveB = recursiveA; + `, + 'A.ts': ` + import {Pipe} from './directives'; + + @Pipe({name: 'A', pure: false}) + export class A {}`, + 'B.ts': ` + import {Pipe} from './directives'; + import {someName, someBool} from './consts'; + + @Pipe({name: someName, pure: someBool}) + export class B {}`, + 'const_expr.ts': ` + function CONST_EXPR(value: any) { return value; } + export var bTrue = CONST_EXPR(true); + export var bFalse = CONST_EXPR(false); + `, + 'forwardRef.ts': ` + function forwardRef(value: any) { return value; } + export var bTrue = forwardRef(() => true); + export var bFalse = forwardRef(() => false); + `, + 'newExpression.ts': ` + import {Value} from './classes'; + function CONST_EXPR(value: any) { return value; } + function forwardRef(value: any) { return value; } + export const someValue = new Value("name", 12); + export const complex = CONST_EXPR(new Value("name", forwardRef(() => 12))); + `, + 'errors.ts': ` + let f = () => 1; + let e: NotFound; + let s = { 1: 1, 2: 2 }; + let t = typeof 12; + `, + 'declared.ts': ` + declare namespace Foo { + type A = string; + } + + let a: Foo.A = 'some value'; + ` +}; diff --git a/packages/compiler-cli/test/metadata/index_writer_spec.ts b/packages/compiler-cli/test/metadata/index_writer_spec.ts new file mode 100644 index 0000000000..ee0678d6ac --- /dev/null +++ b/packages/compiler-cli/test/metadata/index_writer_spec.ts @@ -0,0 +1,25 @@ +/** + * @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 {MetadataBundler} from '../../src/metadata/bundler'; +import {MetadataCollector} from '../../src/metadata/collector'; +import {privateEntriesToIndex} from '../../src/metadata/index_writer'; + +import {MockStringBundlerHost, SIMPLE_LIBRARY} from './bundler_spec'; + +describe('index_writer', () => { + it('should be able to write the index of a simple library', () => { + const host = new MockStringBundlerHost('/', SIMPLE_LIBRARY); + const bundler = new MetadataBundler('/lib/index', undefined, host); + const bundle = bundler.getMetadataBundle(); + const result = privateEntriesToIndex('./index', bundle.privates); + expect(result).toContain(`export * from './index';`); + expect(result).toContain(`export {PrivateOne as ɵa} from './src/one';`); + expect(result).toContain(`export {PrivateTwo as ɵb} from './src/two/index';`); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/test/metadata/symbols_spec.ts b/packages/compiler-cli/test/metadata/symbols_spec.ts new file mode 100644 index 0000000000..c0cacb0db6 --- /dev/null +++ b/packages/compiler-cli/test/metadata/symbols_spec.ts @@ -0,0 +1,133 @@ +/** + * @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 {isMetadataGlobalReferenceExpression} from '../../src/metadata/schema'; +import {Symbols} from '../../src/metadata/symbols'; + +import {Directory, Host, expectNoDiagnostics} from './typescript.mocks'; + +describe('Symbols', () => { + let symbols: Symbols; + const someValue = 'some-value'; + + beforeEach(() => symbols = new Symbols(null as any as ts.SourceFile)); + + it('should be able to add a symbol', () => symbols.define('someSymbol', someValue)); + + beforeEach(() => symbols.define('someSymbol', someValue)); + + it('should be able to `has` a symbol', () => expect(symbols.has('someSymbol')).toBeTruthy()); + it('should be able to `get` a symbol value', + () => expect(symbols.resolve('someSymbol')).toBe(someValue)); + it('should be able to `get` a symbol value', + () => expect(symbols.resolve('someSymbol')).toBe(someValue)); + it('should be able to determine symbol is missing', + () => expect(symbols.has('missingSymbol')).toBeFalsy()); + it('should return undefined from `get` for a missing symbol', + () => expect(symbols.resolve('missingSymbol')).toBeUndefined()); + + let host: ts.LanguageServiceHost; + let service: ts.LanguageService; + let program: ts.Program; + let expressions: ts.SourceFile; + let imports: ts.SourceFile; + + beforeEach(() => { + host = new Host(FILES, ['consts.ts', 'expressions.ts', 'imports.ts']); + service = ts.createLanguageService(host); + program = service.getProgram(); + expressions = program.getSourceFile('expressions.ts'); + imports = program.getSourceFile('imports.ts'); + }); + + it('should not have syntax errors in the test sources', () => { + expectNoDiagnostics(service.getCompilerOptionsDiagnostics()); + for (const sourceFile of program.getSourceFiles()) { + expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName)); + } + }); + + it('should be able to find the source files', () => { + expect(expressions).toBeDefined(); + expect(imports).toBeDefined(); + }); + + it('should be able to create symbols for a source file', () => { + const symbols = new Symbols(expressions); + expect(symbols).toBeDefined(); + }); + + + it('should be able to find symbols in expression', () => { + const symbols = new Symbols(expressions); + expect(symbols.has('someName')).toBeTruthy(); + expect(symbols.resolve('someName')) + .toEqual({__symbolic: 'reference', module: './consts', name: 'someName'}); + expect(symbols.has('someBool')).toBeTruthy(); + expect(symbols.resolve('someBool')) + .toEqual({__symbolic: 'reference', module: './consts', name: 'someBool'}); + }); + + it('should be able to detect a * import', () => { + const symbols = new Symbols(imports); + expect(symbols.resolve('b')).toEqual({__symbolic: 'reference', module: 'b'}); + }); + + it('should be able to detect importing a default export', () => { + const symbols = new Symbols(imports); + expect(symbols.resolve('d')).toEqual({__symbolic: 'reference', module: 'd', default: true}); + }); + + it('should be able to import a renamed symbol', () => { + const symbols = new Symbols(imports); + expect(symbols.resolve('g')).toEqual({__symbolic: 'reference', name: 'f', module: 'f'}); + }); + + it('should be able to resolve any symbol in core global scope', () => { + const core = program.getSourceFiles().find(source => source.fileName.endsWith('lib.d.ts')); + expect(core).toBeDefined(); + const visit = (node: ts.Node): boolean => { + switch (node.kind) { + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.VariableDeclarationList: + return !!ts.forEachChild(node, visit); + case ts.SyntaxKind.VariableDeclaration: + const variableDeclaration = node; + const nameNode = variableDeclaration.name; + const name = nameNode.text; + const result = symbols.resolve(name); + expect(isMetadataGlobalReferenceExpression(result) && result.name).toEqual(name); + + // Ignore everything after Float64Array as it is IE specific. + return name === 'Float64Array'; + } + return false; + }; + ts.forEachChild(core !, visit); + }); +}); + +const FILES: Directory = { + 'consts.ts': ` + export var someName = 'some-name'; + export var someBool = true; + export var one = 1; + export var two = 2; + `, + 'expressions.ts': ` + import {someName, someBool, one, two} from './consts'; + `, + 'imports.ts': ` + import * as b from 'b'; + import 'c'; + import d from 'd'; + import {f as g} from 'f'; + ` +}; diff --git a/packages/compiler-cli/test/metadata/typescript.mocks.ts b/packages/compiler-cli/test/metadata/typescript.mocks.ts new file mode 100644 index 0000000000..5b95e994c8 --- /dev/null +++ b/packages/compiler-cli/test/metadata/typescript.mocks.ts @@ -0,0 +1,204 @@ +/** + * @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 fs from 'fs'; +import * as ts from 'typescript'; + +export interface Directory { [name: string]: (Directory|string); } + +export class Host implements ts.LanguageServiceHost { + private overrides = new Map(); + private version = 1; + + constructor(private directory: Directory, private scripts: string[]) {} + + getCompilationSettings(): ts.CompilerOptions { + return { + experimentalDecorators: true, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5 + }; + } + + getScriptFileNames(): string[] { return this.scripts; } + + getScriptVersion(fileName: string): string { return this.version.toString(); } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined { + const content = this.getFileContent(fileName); + if (content) return ts.ScriptSnapshot.fromString(content); + } + + fileExists(fileName: string): boolean { return this.getFileContent(fileName) != null; } + + getCurrentDirectory(): string { return '/'; } + + getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; } + + overrideFile(fileName: string, content: string) { + this.overrides.set(fileName, content); + this.version++; + } + + addFile(fileName: string) { + this.scripts.push(fileName); + this.version++; + } + + private getFileContent(fileName: string): string|undefined { + if (this.overrides.has(fileName)) { + return this.overrides.get(fileName); + } + if (fileName.endsWith('lib.d.ts')) { + return fs.readFileSync(ts.getDefaultLibFilePath(this.getCompilationSettings()), 'utf8'); + } + const current = open(this.directory, fileName); + if (typeof current === 'string') return current; + } +} + +export function open(directory: Directory, fileName: string): Directory|string|undefined { + // Path might be normalized by the current node environment. But it could also happen that this + // path directly comes from the compiler in POSIX format. Support both separators for development. + const names = fileName.split(/[\\/]/); + let current: Directory|string = directory; + if (names.length && names[0] === '') names.shift(); + for (const name of names) { + if (!current || typeof current === 'string') return undefined; + current = current[name]; + } + return current; +} + +export class MockNode implements ts.Node { + constructor( + public kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, public flags: ts.NodeFlags = 0, + public pos: number = 0, public end: number = 0) {} + getSourceFile(): ts.SourceFile { return null as any as ts.SourceFile; } + getChildCount(sourceFile?: ts.SourceFile): number { return 0; } + getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node { return null as any as ts.Node; } + getChildren(sourceFile?: ts.SourceFile): ts.Node[] { return []; } + getStart(sourceFile?: ts.SourceFile): number { return 0; } + getFullStart(): number { return 0; } + getEnd(): number { return 0; } + getWidth(sourceFile?: ts.SourceFile): number { return 0; } + getFullWidth(): number { return 0; } + getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number { return 0; } + getFullText(sourceFile?: ts.SourceFile): string { return ''; } + getText(sourceFile?: ts.SourceFile): string { return ''; } + getFirstToken(sourceFile?: ts.SourceFile): ts.Node { return null as any as ts.Node; } + getLastToken(sourceFile?: ts.SourceFile): ts.Node { return null as any as ts.Node; } + forEachChild( + cbNode: (node: ts.Node) => T | undefined, + cbNodeArray?: (nodes: ts.NodeArray) => T | undefined): T|undefined { + return undefined; + } +} + +export class MockIdentifier extends MockNode implements ts.Identifier { + public text: string; + // tslint:disable + public _primaryExpressionBrand: any; + public _memberExpressionBrand: any; + public _leftHandSideExpressionBrand: any; + public _incrementExpressionBrand: any; + public _unaryExpressionBrand: any; + public _expressionBrand: any; + public _updateExpressionBrand: any; + // tslint:enable + + constructor( + public name: string, public kind: ts.SyntaxKind.Identifier = ts.SyntaxKind.Identifier, + flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) { + super(kind, flags, pos, end); + this.text = name; + } +} + +export class MockVariableDeclaration extends MockNode implements ts.VariableDeclaration { + // tslint:disable-next-line + public _declarationBrand: any; + + constructor( + public name: ts.Identifier, + public kind: ts.SyntaxKind.VariableDeclaration = ts.SyntaxKind.VariableDeclaration, + flags: ts.NodeFlags = 0, pos: number = 0, end: number = 0) { + super(kind, flags, pos, end); + } + + static of (name: string): MockVariableDeclaration { + return new MockVariableDeclaration(new MockIdentifier(name)); + } +} + +export class MockSymbol implements ts.Symbol { + constructor( + public name: string, private node: ts.Declaration = MockVariableDeclaration.of(name), + public flags: ts.SymbolFlags = 0) {} + + getFlags(): ts.SymbolFlags { return this.flags; } + getName(): string { return this.name; } + getDeclarations(): ts.Declaration[] { return [this.node]; } + getDocumentationComment(): ts.SymbolDisplayPart[] { return []; } + // TODO(vicb): removed in TS 2.2 + getJsDocTags(): any[] { return []; } + + static of (name: string): MockSymbol { return new MockSymbol(name); } +} + +export function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) { + for (const diagnostic of diagnostics) { + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + if (diagnostic.file && diagnostic.start) { + const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + // tslint:disable-next-line:no-console + console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); + } + } + expect(diagnostics.length).toBe(0); +} + +export function expectValidSources(service: ts.LanguageService, program: ts.Program) { + expectNoDiagnostics(service.getCompilerOptionsDiagnostics()); + for (const sourceFile of program.getSourceFiles()) { + expectNoDiagnostics(service.getSyntacticDiagnostics(sourceFile.fileName)); + expectNoDiagnostics(service.getSemanticDiagnostics(sourceFile.fileName)); + } +} + +export function allChildren(node: ts.Node, cb: (node: ts.Node) => T | undefined): T|undefined { + return ts.forEachChild(node, child => cb(node) || allChildren(child, cb)); +} + +export function findClass(sourceFile: ts.SourceFile, name: string): ts.ClassDeclaration|undefined { + return ts.forEachChild( + sourceFile, node => isClass(node) && isNamed(node.name, name) ? node : undefined); +} + +export function findVar(sourceFile: ts.SourceFile, name: string): ts.VariableDeclaration|undefined { + return allChildren( + sourceFile, node => isVar(node) && isNamed(node.name, name) ? node : undefined); +} + +export function findVarInitializer(sourceFile: ts.SourceFile, name: string): ts.Expression { + const v = findVar(sourceFile, name); + expect(v && v.initializer).toBeDefined(); + return v !.initializer !; +} + +export function isClass(node: ts.Node): node is ts.ClassDeclaration { + return node.kind === ts.SyntaxKind.ClassDeclaration; +} + +export function isNamed(node: ts.Node | undefined, name: string): node is ts.Identifier { + return !!node && node.kind === ts.SyntaxKind.Identifier && (node).text === name; +} + +export function isVar(node: ts.Node): node is ts.VariableDeclaration { + return node.kind === ts.SyntaxKind.VariableDeclaration; +} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 6479c4a019..c1fae5e477 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {mainSync, readCommandLineAndConfiguration, watchMode} from '../src/main'; +import {main, readCommandLineAndConfiguration, watchMode} from '../src/main'; +import {makeTempDir} from './test_support'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -84,7 +84,7 @@ describe('ngc transformer command-line', () => { writeConfig(); write('test.ts', 'export const A = 1;'); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); }); @@ -99,7 +99,7 @@ describe('ngc transformer command-line', () => { "files": ["test.ts"] }`); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledWith( `error TS6053: File '` + path.join(basePath, 'test.ts') + `' not found.` + '\n'); @@ -110,7 +110,7 @@ describe('ngc transformer command-line', () => { writeConfig(); write('test.ts', 'foo;'); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledWith( `test.ts(1,1): error TS2304: Cannot find name 'foo'.` + '\n'); @@ -121,7 +121,7 @@ describe('ngc transformer command-line', () => { writeConfig(); write('test.ts', `import {MyClass} from './not-exist-deps';`); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledWith( `test.ts(1,23): error TS2307: Cannot find module './not-exist-deps'.` + '\n'); @@ -133,7 +133,7 @@ describe('ngc transformer command-line', () => { write('empty-deps.ts', 'export const A = 1;'); write('test.ts', `import {MyClass} from './empty-deps';`); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledWith( `test.ts(1,9): error TS2305: Module '"` + path.join(basePath, 'empty-deps') + `"' has no exported member 'MyClass'.` + @@ -149,7 +149,7 @@ describe('ngc transformer command-line', () => { A(); `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledWith( 'test.ts(3,9): error TS2349: Cannot invoke an expression whose type lacks a call signature. ' + 'Type \'String\' has no compatible call signatures.\n'); @@ -159,7 +159,7 @@ describe('ngc transformer command-line', () => { it('should print the stack trace on compiler internal errors', () => { write('test.ts', 'export const A = 1;'); - const exitCode = mainSync(['-p', 'not-exist'], errorSpy); + const exitCode = main(['-p', 'not-exist'], errorSpy); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); @@ -181,7 +181,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]) .toContain('Error at ng://' + path.join(basePath, 'mymodule.ts.MyComp.html')); @@ -212,7 +212,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]) .toContain('Error at ng://' + path.join(basePath, 'my.component.html(1,5):')); @@ -240,7 +240,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); @@ -265,7 +265,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); expect(fs.existsSync(path.resolve( @@ -367,7 +367,7 @@ describe('ngc transformer command-line', () => { }, "include": ["src/**/*.ts"] }`); - const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); outDir = path.resolve(basePath, 'built', 'src'); expectJsDtsMetadataJsonToExist(); @@ -387,7 +387,7 @@ describe('ngc transformer command-line', () => { }, "include": ["src/**/*.ts"] }`); - let exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + let exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); outDir = path.resolve(basePath, 'lib', 'src'); modules.forEach(moduleName => { @@ -420,7 +420,7 @@ describe('ngc transformer command-line', () => { }`); write('lib/src/plain.css', 'div {}'); write('lib/src/emulated.css', 'div {}'); - exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); outDir = path.resolve(basePath, 'built', 'lib', 'src'); expectAllGeneratedFilesToExist(); @@ -443,7 +443,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -472,7 +472,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -501,7 +501,7 @@ describe('ngc transformer command-line', () => { export class MyModule {} `); - const exitCode = mainSync(['-p', basePath], errorSpy); + const exitCode = main(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -519,7 +519,8 @@ describe('ngc transformer command-line', () => { }); function compile(): number { - const result = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + errorSpy.calls.reset(); + const result = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(errorSpy).not.toHaveBeenCalled(); return result; } @@ -710,7 +711,7 @@ describe('ngc transformer command-line', () => { } `); - expect(mainSync(['-p', basePath], errorSpy)).toBe(0); + expect(main(['-p', basePath], errorSpy)).toBe(0); shouldExist('module.js'); }); }); @@ -756,7 +757,7 @@ describe('ngc transformer command-line', () => { export class FlatModule { }`); - const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); shouldExist('index.js'); shouldExist('index.metadata.json'); @@ -794,7 +795,7 @@ describe('ngc transformer command-line', () => { }); it('should compile without error', () => { - expect(mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0); + expect(main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0); }); }); @@ -883,10 +884,10 @@ describe('ngc transformer command-line', () => { } `); - expect(mainSync(['-p', path.join(basePath, 'tsconfig-ng.json')], errorSpy)).toBe(0); - expect(mainSync(['-p', path.join(basePath, 'lib1', 'tsconfig-lib1.json')], errorSpy)).toBe(0); - expect(mainSync(['-p', path.join(basePath, 'lib2', 'tsconfig-lib2.json')], errorSpy)).toBe(0); - expect(mainSync(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0); + expect(main(['-p', path.join(basePath, 'tsconfig-ng.json')], errorSpy)).toBe(0); + expect(main(['-p', path.join(basePath, 'lib1', 'tsconfig-lib1.json')], errorSpy)).toBe(0); + expect(main(['-p', path.join(basePath, 'lib2', 'tsconfig-lib2.json')], errorSpy)).toBe(0); + expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0); // library 1 // make `shouldExist` / `shouldNotExist` relative to `node_modules` @@ -915,6 +916,80 @@ describe('ngc transformer command-line', () => { }); }); + describe('expression lowering', () => { + const shouldExist = (fileName: string) => { + if (!fs.existsSync(path.resolve(basePath, fileName))) { + throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`); + } + }; + + it('should be able to lower supported expressions', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["module.ts"] + }`); + write('module.ts', ` + import {NgModule, InjectionToken} from '@angular/core'; + import {AppComponent} from './app'; + + export interface Info { + route: string; + data: string; + } + + export const T1 = new InjectionToken('t1'); + export const T2 = new InjectionToken('t2'); + export const T3 = new InjectionToken('t3'); + export const T4 = new InjectionToken('t4'); + + enum SomeEnum { + OK, + Cancel + } + + function calculateString() { + return 'someValue'; + } + + const routeLikeData = [{ + route: '/home', + data: calculateString() + }]; + + @NgModule({ + declarations: [AppComponent], + providers: [ + { provide: T1, useValue: calculateString() }, + { provide: T2, useFactory: () => 'someValue' }, + { provide: T3, useValue: SomeEnum.OK }, + { provide: T4, useValue: routeLikeData } + ] + }) + export class MyModule {} + `); + write('app.ts', ` + import {Component, Inject} from '@angular/core'; + import * as m from './module'; + + @Component({ + selector: 'my-app', + template: '' + }) + export class AppComponent { + constructor( + @Inject(m.T1) private t1: string, + @Inject(m.T2) private t2: string, + @Inject(m.T3) private t3: number, + @Inject(m.T4) private t4: m.Info[], + ) {} + } + `); + + expect(main(['-p', basePath], s => {})).toBe(0); + shouldExist('built/module.js'); + }); + }); + describe('watch mode', () => { let timer: (() => void)|undefined = undefined; let results: ((message: string) => void)|undefined = undefined; diff --git a/packages/compiler-cli/test/test_support.ts b/packages/compiler-cli/test/test_support.ts new file mode 100644 index 0000000000..feb27463bd --- /dev/null +++ b/packages/compiler-cli/test/test_support.ts @@ -0,0 +1,28 @@ +/** + * @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 fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +const tmpdir = process.env.TEST_TMPDIR || os.tmpdir(); + +export function writeTempFile(name: string, contents: string): string { + // TEST_TMPDIR is set by bazel. + const id = (Math.random() * 1000000).toFixed(0); + const fn = path.join(tmpdir, `tmp.${id}.${name}`); + fs.writeFileSync(fn, contents); + return fn; +} + +export function makeTempDir(): string { + const id = (Math.random() * 1000000).toFixed(0); + const dir = path.join(tmpdir, `tmp.${id}`); + fs.mkdirSync(dir); + return dir; +} \ No newline at end of file diff --git a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts index 584576f800..8b79a5761b 100644 --- a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts +++ b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {ModuleMetadata} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; +import {ModuleMetadata} from '../../src/metadata/index'; import {LowerMetadataCache, LoweringRequest, RequestLocationMap, getExpressionLoweringTransformFactory} from '../../src/transformers/lower_expressions'; import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index 2fc38bb195..40f3d3003f 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -7,8 +7,9 @@ */ import {AotSummaryResolver, GeneratedFile, StaticSymbolCache, StaticSymbolResolver, toTypeScript} from '@angular/compiler'; +import {MetadataBundler} from '@angular/compiler-cli/src/metadata/bundler'; +import {privateEntriesToIndex} from '@angular/compiler-cli/src/metadata/index_writer'; import {NodeFlags} from '@angular/core/src/view/index'; -import {MetadataBundler, privateEntriesToIndex} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; import {extractSourceMap, originalPositionFor} from '../output/source_map_util'; diff --git a/packages/compiler/test/aot/static_reflector_spec.ts b/packages/compiler/test/aot/static_reflector_spec.ts index 8a1824ad6e..97b44f87b0 100644 --- a/packages/compiler/test/aot/static_reflector_spec.ts +++ b/packages/compiler/test/aot/static_reflector_spec.ts @@ -7,7 +7,7 @@ */ import {StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, core as compilerCore} from '@angular/compiler'; -import {CollectorOptions} from '@angular/tsc-wrapped'; +import {CollectorOptions} from '@angular/compiler-cli/src/metadata/index'; import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec'; diff --git a/packages/compiler/test/aot/static_symbol_resolver_spec.ts b/packages/compiler/test/aot/static_symbol_resolver_spec.ts index facf5f7f51..8025b9c149 100644 --- a/packages/compiler/test/aot/static_symbol_resolver_spec.ts +++ b/packages/compiler/test/aot/static_symbol_resolver_spec.ts @@ -7,7 +7,8 @@ */ import {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler'; -import {CollectorOptions, MetadataCollector} from '@angular/tsc-wrapped'; +import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector'; +import {CollectorOptions} from '@angular/compiler-cli/src/metadata/index'; import * as ts from 'typescript'; diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index d9a0a18847..2e54338f45 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -7,7 +7,9 @@ */ import {AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler, toTypeScript} from '@angular/compiler'; -import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; +import {MetadataBundlerHost} from '@angular/compiler-cli/src/metadata/bundler'; +import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector'; +import {ModuleMetadata} from '@angular/compiler-cli/src/metadata/index'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; diff --git a/packages/language-service/src/reflector_host.ts b/packages/language-service/src/reflector_host.ts index f9439f1c51..c78787b845 100644 --- a/packages/language-service/src/reflector_host.ts +++ b/packages/language-service/src/reflector_host.ts @@ -7,7 +7,7 @@ */ import {AotCompilerHost} from '@angular/compiler'; -import {AngularCompilerOptions, CompilerHost, ModuleResolutionHostAdapter} from '@angular/compiler-cli/src/language_services'; +import {CompilerHost, CompilerOptions, ModuleResolutionHostAdapter} from '@angular/compiler-cli/src/language_services'; import * as ts from 'typescript'; class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost { @@ -37,7 +37,7 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost { export class ReflectorHost extends CompilerHost { constructor( private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost, - options: AngularCompilerOptions) { + options: CompilerOptions) { super( // The ancestor value for program is overridden below so passing null here is safe. /* program */ null !, options, diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 930b59e12f..3a71cf6f28 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -7,7 +7,7 @@ */ import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; -import {AngularCompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; +import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -372,7 +372,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost { const tsConfigPath = findTsConfig(source.fileName); const basePath = path.dirname(tsConfigPath || this.context); - const options: AngularCompilerOptions = {basePath, genDir: basePath}; + const options: CompilerOptions = {basePath, genDir: basePath}; const compilerOptions = this.host.getCompilationSettings(); if (compilerOptions && compilerOptions.baseUrl) { options.baseUrl = compilerOptions.baseUrl; diff --git a/tools/build/linknodemodules.js b/tools/build/linknodemodules.js deleted file mode 100644 index 14a49c7948..0000000000 --- a/tools/build/linknodemodules.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @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 - */ - -var fs = require('fs'); -var path = require('path'); - -module.exports = function(gulp, plugins, config) { - function symlink(relativeFolder, linkDir) { - var sourceDir = path.join('..', relativeFolder); - if (!fs.existsSync(linkDir)) { - console.log('creating link', linkDir, sourceDir); - try { - fs.symlinkSync(sourceDir, linkDir, 'dir'); - } catch (e) { - var sourceDir = path.join(config.dir, relativeFolder); - console.log('linking failed: trying to hard copy', linkDir, sourceDir); - copyRecursiveSync(sourceDir, linkDir); - } - } - } - - return function() { - var nodeModulesDir = path.join(config.dir, 'node_modules'); - if (!fs.existsSync(nodeModulesDir)) { - fs.mkdirSync(nodeModulesDir); - } - getSubdirs(config.dir).forEach(function(relativeFolder) { - if (relativeFolder === 'node_modules') { - return; - } - - var linkDir = path.join(nodeModulesDir, relativeFolder); - symlink(relativeFolder, linkDir); - }); - // Also symlink tools we release independently to NPM, so tests can require metadata, etc. - symlink('../../tools/metadata', path.join(nodeModulesDir, 'tsc-wrapped')); - }; -}; - -function copyRecursiveSync(src, dest) { - if (fs.existsSync(src)) { - var stats = fs.statSync(src); - if (stats.isDirectory()) { - fs.mkdirSync(dest); - fs.readdirSync(src).forEach(function(childItemName) { - copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - fs.writeFileSync(dest, fs.readFileSync(src)); - } - } -} - -function getSubdirs(rootDir) { - return fs.readdirSync(rootDir).filter(function(file) { - if (file[0] === '.') { - return false; - } - var dirPath = path.join(rootDir, file); - return fs.statSync(dirPath).isDirectory(); - }); -}