diff --git a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts index 95f403a6ee..da44565c41 100644 --- a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts +++ b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts @@ -12,7 +12,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols'; - +import {isVersionBetween} from './typescript_version'; // In TypeScript 2.1 these flags moved // These helpers work for both 2.0 and 2.1. @@ -394,21 +394,33 @@ class SignatureResultOverride implements Signature { get result(): Symbol { return this.resultType; } } -const toSymbolTable: (symbols: ts.Symbol[]) => ts.SymbolTable = isTypescriptVersion('2.2') ? - (symbols => { - const result = new Map(); - for (const symbol of symbols) { - result.set(symbol.name, symbol); - } - return (result as any); - }) : - (symbols => { - const result = {}; - for (const symbol of symbols) { - result[symbol.name] = symbol; - } - return result as ts.SymbolTable; - }); +/** + * Indicates the lower bound TypeScript version supporting `SymbolTable` as an ES6 `Map`. + * For lower versions, `SymbolTable` is implemented as a dictionary + */ +const MIN_TS_VERSION_SUPPORTING_MAP = '2.2'; + +export const toSymbolTableFactory = (tsVersion: string) => (symbols: ts.Symbol[]) => { + if (isVersionBetween(tsVersion, MIN_TS_VERSION_SUPPORTING_MAP)) { + // ∀ Typescript version >= 2.2, `SymbolTable` is implemented as an ES6 `Map` + const result = new Map(); + for (const symbol of symbols) { + result.set(symbol.name, symbol); + } + // First, tell the compiler that `result` is of type `any`. Then, use a second type assertion + // to `ts.SymbolTable`. + // Otherwise, `Map` and `ts.SymbolTable` will be considered as incompatible + // types by the compiler + return (result); + } + + // ∀ Typescript version < 2.2, `SymbolTable` is implemented as a dictionary + const result: {[name: string]: ts.Symbol} = {}; + for (const symbol of symbols) { + result[symbol.name] = symbol; + } + return (result); +}; function toSymbols(symbolTable: ts.SymbolTable | undefined): ts.Symbol[] { if (!symbolTable) return []; @@ -442,6 +454,7 @@ class SymbolTableWrapper implements SymbolTable { if (Array.isArray(symbols)) { this.symbols = symbols; + const toSymbolTable = toSymbolTableFactory(ts.version); this.symbolTable = toSymbolTable(symbols); } else { this.symbols = toSymbols(symbols); @@ -855,22 +868,3 @@ function getFromSymbolTable(symbolTable: ts.SymbolTable, key: string): ts.Symbol return symbol; } - -function toNumbers(value: string | undefined): number[] { - return value ? value.split('.').map(v => +v) : []; -} - -function compareNumbers(a: number[], b: number[]): -1|0|1 { - for (let i = 0; i < a.length && i < b.length; i++) { - if (a[i] > b[i]) return 1; - if (a[i] < b[i]) return -1; - } - return 0; -} - -function isTypescriptVersion(low: string, high?: string): boolean { - const tsNumbers = toNumbers(ts.version); - - return compareNumbers(toNumbers(low), tsNumbers) <= 0 && - compareNumbers(toNumbers(high), tsNumbers) >= 0; -} diff --git a/packages/compiler-cli/src/diagnostics/typescript_version.ts b/packages/compiler-cli/src/diagnostics/typescript_version.ts new file mode 100644 index 0000000000..3ca5b79842 --- /dev/null +++ b/packages/compiler-cli/src/diagnostics/typescript_version.ts @@ -0,0 +1,85 @@ +/** + * @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 + */ + +/** + * Converts a `string` version into an array of numbers + * @example + * toNumbers('2.0.1'); // returns [2, 0, 1] + */ +export function toNumbers(value: string): number[] { + return value.split('.').map(Number); +} + +/** + * Compares two arrays of positive numbers with lexicographical order in mind. + * + * However - unlike lexicographical order - for arrays of different length we consider: + * [1, 2, 3] = [1, 2, 3, 0] instead of [1, 2, 3] < [1, 2, 3, 0] + * + * @param a The 'left hand' array in the comparison test + * @param b The 'right hand' in the comparison test + * @returns {-1|0|1} The comparison result: 1 if a is greater, -1 if b is greater, 0 is the two + * arrays are equals + */ +export function compareNumbers(a: number[], b: number[]): -1|0|1 { + const max = Math.max(a.length, b.length); + const min = Math.min(a.length, b.length); + + for (let i = 0; i < min; i++) { + if (a[i] > b[i]) return 1; + if (a[i] < b[i]) return -1; + } + + if (min !== max) { + const longestArray = a.length === max ? a : b; + + // The result to return in case the to arrays are considered different (1 if a is greater, + // -1 if b is greater) + const comparisonResult = a.length === max ? 1 : -1; + + // Check that at least one of the remaining elements is greater than 0 to consider that the two + // arrays are different (e.g. [1, 0] and [1] are considered the same but not [1, 0, 1] and [1]) + for (let i = min; i < max; i++) { + if (longestArray[i] > 0) { + return comparisonResult; + } + } + } + + return 0; +} + +/** + * Checks if a TypeScript version is: + * - greater or equal than the provided `low` version, + * - lower or equal than an optional `high` version. + * + * @param version The TypeScript version + * @param low The minimum version + * @param high The maximum version + */ +export function isVersionBetween(version: string, low: string, high?: string): boolean { + const tsNumbers = toNumbers(version); + if (high !== undefined) { + return compareNumbers(toNumbers(low), tsNumbers) <= 0 && + compareNumbers(toNumbers(high), tsNumbers) >= 0; + } + return compareNumbers(toNumbers(low), tsNumbers) <= 0; +} + +/** + * Compares two versions + * + * @param v1 The 'left hand' version in the comparison test + * @param v2 The 'right hand' version in the comparison test + * @returns {-1|0|1} The comparison result: 1 if v1 is greater, -1 if v2 is greater, 0 is the two + * versions are equals + */ +export function compareVersions(v1: string, v2: string): -1|0|1 { + return compareNumbers(toNumbers(v1), toNumbers(v2)); +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index cc71acb61c..b10c8b8027 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -13,6 +13,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics'; +import {compareVersions} from '../diagnostics/typescript_version'; import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index'; import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api'; @@ -95,6 +96,19 @@ const defaultEmitCallback: TsEmitCallback = program.emit( targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); +/** + * Minimum supported TypeScript version + * ∀ supported typescript version v, v >= MIN_TS_VERSION + */ +const MIN_TS_VERSION = '2.7.2'; + +/** + * Supremum of supported TypeScript versions + * ∀ supported typescript version v, v < MAX_TS_VERSION + * MAX_TS_VERSION is not considered as a supported TypeScript version + */ +const MAX_TS_VERSION = '2.8.0'; + class AngularCompilerProgram implements Program { private rootNames: string[]; private metadataCache: MetadataCache; @@ -124,10 +138,7 @@ class AngularCompilerProgram implements Program { private host: CompilerHost, oldProgram?: Program) { this.rootNames = [...rootNames]; - if ((ts.version < '2.7.2' || ts.version >= '2.8.0') && !options.disableTypeScriptVersionCheck) { - throw new Error( - `The Angular Compiler requires TypeScript >=2.7.2 and <2.8.0 but ${ts.version} was found instead.`); - } + checkVersion(ts.version, MIN_TS_VERSION, MAX_TS_VERSION, options.disableTypeScriptVersionCheck); this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; if (oldProgram) { @@ -847,6 +858,34 @@ class AngularCompilerProgram implements Program { } } +/** + * Checks whether a given version ∈ [minVersion, maxVersion[ + * An error will be thrown if the following statements are simultaneously true: + * - the given version ∉ [minVersion, maxVersion[, + * - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck + * is false) + * + * @param version The version on which the check will be performed + * @param minVersion The lower bound version. A valid version needs to be greater than minVersion + * @param maxVersion The upper bound version. A valid version needs to be strictly less than + * maxVersion + * @param disableVersionCheck Indicates whether version check should be bypassed + * + * @throws Will throw an error if the following statements are simultaneously true: + * - the given version ∉ [minVersion, maxVersion[, + * - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck + * is false) + */ +export function checkVersion( + version: string, minVersion: string, maxVersion: string, + disableVersionCheck: boolean | undefined) { + if ((compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0) && + !disableVersionCheck) { + throw new Error( + `The Angular Compiler requires TypeScript >=${minVersion} and <${maxVersion} but ${version} was found instead.`); + } +} + export function createProgram({rootNames, options, host, oldProgram}: { rootNames: ReadonlyArray, options: CompilerOptions, diff --git a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts b/packages/compiler-cli/test/diagnostics/typescript_symbols_spec.ts similarity index 83% rename from packages/compiler-cli/test/diagnostics/symbol_query_spec.ts rename to packages/compiler-cli/test/diagnostics/typescript_symbols_spec.ts index 09d536e643..bef8bd2fd3 100644 --- a/packages/compiler-cli/test/diagnostics/symbol_query_spec.ts +++ b/packages/compiler-cli/test/diagnostics/typescript_symbols_spec.ts @@ -13,7 +13,7 @@ import {ReflectorHost} from '@angular/language-service/src/reflector_host'; import * as ts from 'typescript'; import {Symbol, SymbolQuery, SymbolTable} from '../../src/diagnostics/symbols'; -import {getSymbolQuery} from '../../src/diagnostics/typescript_symbols'; +import {getSymbolQuery, toSymbolTableFactory} from '../../src/diagnostics/typescript_symbols'; import {CompilerOptions} from '../../src/transformers/api'; import {Directory} from '../mocks'; @@ -57,6 +57,19 @@ describe('symbol query', () => { }); }); +describe('toSymbolTableFactory(tsVersion)', () => { + it('should return a Map for versions of TypeScript >= 2.2 and a dictionary otherwise', () => { + const a = { name: 'a' } as ts.Symbol; + const b = { name: 'b' } as ts.Symbol; + + expect(toSymbolTableFactory('2.1')([a, b]) instanceof Map).toEqual(false); + expect(toSymbolTableFactory('2.4')([a, b]) instanceof Map).toEqual(true); + + // Check that for the lower bound version `2.2`, toSymbolTableFactory('2.2') returns a map + expect(toSymbolTableFactory('2.2')([a, b]) instanceof Map).toEqual(true); + }); +}); + function appComponentSource(template: string): string { return ` import {Component} from '@angular/core'; diff --git a/packages/compiler-cli/test/diagnostics/typescript_version_spec.ts b/packages/compiler-cli/test/diagnostics/typescript_version_spec.ts new file mode 100644 index 0000000000..605b3e1ed7 --- /dev/null +++ b/packages/compiler-cli/test/diagnostics/typescript_version_spec.ts @@ -0,0 +1,70 @@ +/** + * @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 {compareNumbers, compareVersions, isVersionBetween, toNumbers} from '../../src/diagnostics/typescript_version'; + + +describe('toNumbers', () => { + it('should handle strings', () => { + expect(toNumbers('2')).toEqual([2]); + expect(toNumbers('2.1')).toEqual([2, 1]); + expect(toNumbers('2.0.1')).toEqual([2, 0, 1]); + }); +}); + +describe('compareNumbers', () => { + + it('should handle empty arrays', () => { expect(compareNumbers([], [])).toEqual(0); }); + + it('should handle arrays of same length', () => { + expect(compareNumbers([1], [3])).toEqual(-1); + expect(compareNumbers([3], [1])).toEqual(1); + + expect(compareNumbers([1, 0], [1, 0])).toEqual(0); + + expect(compareNumbers([1, 1], [1, 0])).toEqual(1); + expect(compareNumbers([1, 0], [1, 1])).toEqual(-1); + + expect(compareNumbers([1, 0, 9], [1, 1, 0])).toEqual(-1); + expect(compareNumbers([1, 1, 0], [1, 0, 9])).toEqual(1); + }); + + it('should handle arrays of different length', () => { + expect(compareNumbers([2], [2, 1])).toEqual(-1); + expect(compareNumbers([2, 1], [2])).toEqual(1); + + expect(compareNumbers([0, 9], [1])).toEqual(-1); + expect(compareNumbers([1], [0, 9])).toEqual(1); + + expect(compareNumbers([2], [])).toEqual(1); + expect(compareNumbers([], [2])).toEqual(-1); + + expect(compareNumbers([1, 0], [1, 0, 0, 0])).toEqual(0); + }); +}); + +describe('isVersionBetween', () => { + it('should correctly check if a typescript version is within a given range', () => { + expect(isVersionBetween('2.7.0', '2.40')).toEqual(false); + expect(isVersionBetween('2.40', '2.7.0')).toEqual(true); + + expect(isVersionBetween('2.7.2', '2.7.0', '2.8.0')).toEqual(true); + + expect(isVersionBetween('2.7.2', '2.7.7', '2.8.0')).toEqual(false); + }); +}); + +describe('compareVersions', () => { + it('should correctly compare versions', () => { + expect(compareVersions('2.7.0', '2.40')).toEqual(-1); + expect(compareVersions('2.40', '2.7.0')).toEqual(1); + expect(compareVersions('2.40', '2.40')).toEqual(0); + expect(compareVersions('2.40', '2.41')).toEqual(-1); + expect(compareVersions('2', '2.1')).toEqual(-1); + }); +}); diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts index bbf7c526ae..6fee0ea657 100644 --- a/packages/compiler-cli/test/transformers/program_spec.ts +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -13,7 +13,7 @@ import * as ts from 'typescript'; import {formatDiagnostics} from '../../src/perform_compile'; import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api'; -import {createSrcToOutPathMapper} from '../../src/transformers/program'; +import {checkVersion, createSrcToOutPathMapper} from '../../src/transformers/program'; import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util'; import {TestSupport, expectNoDiagnosticsInProgram, isInBazel, setup} from '../test_support'; @@ -1048,4 +1048,35 @@ describe('ng program', () => { .toContain('Function expressions are not supported'); }); }); + + describe('checkVersion', () => { + const MIN_TS_VERSION = '2.7.2'; + const MAX_TS_VERSION = '2.8.0'; + + const versionError = (version: string) => + `The Angular Compiler requires TypeScript >=${MIN_TS_VERSION} and <${MAX_TS_VERSION} but ${version} was found instead.`; + + it('should not throw when a supported TypeScript version is used', () => { + expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, undefined)).not.toThrow(); + expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, false)).not.toThrow(); + expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow(); + }); + + it('should handle a TypeScript version < the minimum supported one', () => { + expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, undefined)) + .toThrowError(versionError('2.4.1')); + expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, false)) + .toThrowError(versionError('2.4.1')); + expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow(); + }); + + it('should handle a TypeScript version > the maximum supported one', () => { + expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, undefined)) + .toThrowError(versionError('2.9.0')); + expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, false)) + .toThrowError(versionError('2.9.0')); + expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow(); + }); + }); + });