Skip to content

Commit 193737a

Browse files
benbraoualxhub
authored andcommitted
fix(compiler-cli): use numeric comparison for TypeScript version (#22705)
Fixes #22593 PR Close #22705
1 parent be10bf5 commit 193737a

File tree

6 files changed

+273
-41
lines changed

6 files changed

+273
-41
lines changed

packages/compiler-cli/src/diagnostics/typescript_symbols.ts

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import * as path from 'path';
1212
import * as ts from 'typescript';
1313

1414
import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';
15-
15+
import {isVersionBetween} from './typescript_version';
1616

1717
// In TypeScript 2.1 these flags moved
1818
// These helpers work for both 2.0 and 2.1.
@@ -394,21 +394,33 @@ class SignatureResultOverride implements Signature {
394394
get result(): Symbol { return this.resultType; }
395395
}
396396

397-
const toSymbolTable: (symbols: ts.Symbol[]) => ts.SymbolTable = isTypescriptVersion('2.2') ?
398-
(symbols => {
399-
const result = new Map<string, ts.Symbol>();
400-
for (const symbol of symbols) {
401-
result.set(symbol.name, symbol);
402-
}
403-
return <ts.SymbolTable>(result as any);
404-
}) :
405-
(symbols => {
406-
const result = <any>{};
407-
for (const symbol of symbols) {
408-
result[symbol.name] = symbol;
409-
}
410-
return result as ts.SymbolTable;
411-
});
397+
/**
398+
* Indicates the lower bound TypeScript version supporting `SymbolTable` as an ES6 `Map`.
399+
* For lower versions, `SymbolTable` is implemented as a dictionary
400+
*/
401+
const MIN_TS_VERSION_SUPPORTING_MAP = '2.2';
402+
403+
export const toSymbolTableFactory = (tsVersion: string) => (symbols: ts.Symbol[]) => {
404+
if (isVersionBetween(tsVersion, MIN_TS_VERSION_SUPPORTING_MAP)) {
405+
// ∀ Typescript version >= 2.2, `SymbolTable` is implemented as an ES6 `Map`
406+
const result = new Map<string, ts.Symbol>();
407+
for (const symbol of symbols) {
408+
result.set(symbol.name, symbol);
409+
}
410+
// First, tell the compiler that `result` is of type `any`. Then, use a second type assertion
411+
// to `ts.SymbolTable`.
412+
// Otherwise, `Map<string, ts.Symbol>` and `ts.SymbolTable` will be considered as incompatible
413+
// types by the compiler
414+
return <ts.SymbolTable>(<any>result);
415+
}
416+
417+
// ∀ Typescript version < 2.2, `SymbolTable` is implemented as a dictionary
418+
const result: {[name: string]: ts.Symbol} = {};
419+
for (const symbol of symbols) {
420+
result[symbol.name] = symbol;
421+
}
422+
return <ts.SymbolTable>(<any>result);
423+
};
412424

413425
function toSymbols(symbolTable: ts.SymbolTable | undefined): ts.Symbol[] {
414426
if (!symbolTable) return [];
@@ -442,6 +454,7 @@ class SymbolTableWrapper implements SymbolTable {
442454

443455
if (Array.isArray(symbols)) {
444456
this.symbols = symbols;
457+
const toSymbolTable = toSymbolTableFactory(ts.version);
445458
this.symbolTable = toSymbolTable(symbols);
446459
} else {
447460
this.symbols = toSymbols(symbols);
@@ -855,22 +868,3 @@ function getFromSymbolTable(symbolTable: ts.SymbolTable, key: string): ts.Symbol
855868

856869
return symbol;
857870
}
858-
859-
function toNumbers(value: string | undefined): number[] {
860-
return value ? value.split('.').map(v => +v) : [];
861-
}
862-
863-
function compareNumbers(a: number[], b: number[]): -1|0|1 {
864-
for (let i = 0; i < a.length && i < b.length; i++) {
865-
if (a[i] > b[i]) return 1;
866-
if (a[i] < b[i]) return -1;
867-
}
868-
return 0;
869-
}
870-
871-
function isTypescriptVersion(low: string, high?: string): boolean {
872-
const tsNumbers = toNumbers(ts.version);
873-
874-
return compareNumbers(toNumbers(low), tsNumbers) <= 0 &&
875-
compareNumbers(toNumbers(high), tsNumbers) >= 0;
876-
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Converts a `string` version into an array of numbers
11+
* @example
12+
* toNumbers('2.0.1'); // returns [2, 0, 1]
13+
*/
14+
export function toNumbers(value: string): number[] {
15+
return value.split('.').map(Number);
16+
}
17+
18+
/**
19+
* Compares two arrays of positive numbers with lexicographical order in mind.
20+
*
21+
* However - unlike lexicographical order - for arrays of different length we consider:
22+
* [1, 2, 3] = [1, 2, 3, 0] instead of [1, 2, 3] < [1, 2, 3, 0]
23+
*
24+
* @param a The 'left hand' array in the comparison test
25+
* @param b The 'right hand' in the comparison test
26+
* @returns {-1|0|1} The comparison result: 1 if a is greater, -1 if b is greater, 0 is the two
27+
* arrays are equals
28+
*/
29+
export function compareNumbers(a: number[], b: number[]): -1|0|1 {
30+
const max = Math.max(a.length, b.length);
31+
const min = Math.min(a.length, b.length);
32+
33+
for (let i = 0; i < min; i++) {
34+
if (a[i] > b[i]) return 1;
35+
if (a[i] < b[i]) return -1;
36+
}
37+
38+
if (min !== max) {
39+
const longestArray = a.length === max ? a : b;
40+
41+
// The result to return in case the to arrays are considered different (1 if a is greater,
42+
// -1 if b is greater)
43+
const comparisonResult = a.length === max ? 1 : -1;
44+
45+
// Check that at least one of the remaining elements is greater than 0 to consider that the two
46+
// arrays are different (e.g. [1, 0] and [1] are considered the same but not [1, 0, 1] and [1])
47+
for (let i = min; i < max; i++) {
48+
if (longestArray[i] > 0) {
49+
return comparisonResult;
50+
}
51+
}
52+
}
53+
54+
return 0;
55+
}
56+
57+
/**
58+
* Checks if a TypeScript version is:
59+
* - greater or equal than the provided `low` version,
60+
* - lower or equal than an optional `high` version.
61+
*
62+
* @param version The TypeScript version
63+
* @param low The minimum version
64+
* @param high The maximum version
65+
*/
66+
export function isVersionBetween(version: string, low: string, high?: string): boolean {
67+
const tsNumbers = toNumbers(version);
68+
if (high !== undefined) {
69+
return compareNumbers(toNumbers(low), tsNumbers) <= 0 &&
70+
compareNumbers(toNumbers(high), tsNumbers) >= 0;
71+
}
72+
return compareNumbers(toNumbers(low), tsNumbers) <= 0;
73+
}
74+
75+
/**
76+
* Compares two versions
77+
*
78+
* @param v1 The 'left hand' version in the comparison test
79+
* @param v2 The 'right hand' version in the comparison test
80+
* @returns {-1|0|1} The comparison result: 1 if v1 is greater, -1 if v2 is greater, 0 is the two
81+
* versions are equals
82+
*/
83+
export function compareVersions(v1: string, v2: string): -1|0|1 {
84+
return compareNumbers(toNumbers(v1), toNumbers(v2));
85+
}

packages/compiler-cli/src/transformers/program.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as path from 'path';
1313
import * as ts from 'typescript';
1414

1515
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
16+
import {compareVersions} from '../diagnostics/typescript_version';
1617
import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index';
1718

1819
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 =
9596
program.emit(
9697
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
9798

99+
/**
100+
* Minimum supported TypeScript version
101+
* ∀ supported typescript version v, v >= MIN_TS_VERSION
102+
*/
103+
const MIN_TS_VERSION = '2.7.2';
104+
105+
/**
106+
* Supremum of supported TypeScript versions
107+
* ∀ supported typescript version v, v < MAX_TS_VERSION
108+
* MAX_TS_VERSION is not considered as a supported TypeScript version
109+
*/
110+
const MAX_TS_VERSION = '2.8.0';
111+
98112
class AngularCompilerProgram implements Program {
99113
private rootNames: string[];
100114
private metadataCache: MetadataCache;
@@ -124,10 +138,7 @@ class AngularCompilerProgram implements Program {
124138
private host: CompilerHost, oldProgram?: Program) {
125139
this.rootNames = [...rootNames];
126140

127-
if ((ts.version < '2.7.2' || ts.version >= '2.8.0') && !options.disableTypeScriptVersionCheck) {
128-
throw new Error(
129-
`The Angular Compiler requires TypeScript >=2.7.2 and <2.8.0 but ${ts.version} was found instead.`);
130-
}
141+
checkVersion(ts.version, MIN_TS_VERSION, MAX_TS_VERSION, options.disableTypeScriptVersionCheck);
131142

132143
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
133144
if (oldProgram) {
@@ -847,6 +858,34 @@ class AngularCompilerProgram implements Program {
847858
}
848859
}
849860

861+
/**
862+
* Checks whether a given version ∈ [minVersion, maxVersion[
863+
* An error will be thrown if the following statements are simultaneously true:
864+
* - the given version ∉ [minVersion, maxVersion[,
865+
* - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck
866+
* is false)
867+
*
868+
* @param version The version on which the check will be performed
869+
* @param minVersion The lower bound version. A valid version needs to be greater than minVersion
870+
* @param maxVersion The upper bound version. A valid version needs to be strictly less than
871+
* maxVersion
872+
* @param disableVersionCheck Indicates whether version check should be bypassed
873+
*
874+
* @throws Will throw an error if the following statements are simultaneously true:
875+
* - the given version ∉ [minVersion, maxVersion[,
876+
* - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck
877+
* is false)
878+
*/
879+
export function checkVersion(
880+
version: string, minVersion: string, maxVersion: string,
881+
disableVersionCheck: boolean | undefined) {
882+
if ((compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0) &&
883+
!disableVersionCheck) {
884+
throw new Error(
885+
`The Angular Compiler requires TypeScript >=${minVersion} and <${maxVersion} but ${version} was found instead.`);
886+
}
887+
}
888+
850889
export function createProgram({rootNames, options, host, oldProgram}: {
851890
rootNames: ReadonlyArray<string>,
852891
options: CompilerOptions,

packages/compiler-cli/test/diagnostics/symbol_query_spec.ts renamed to packages/compiler-cli/test/diagnostics/typescript_symbols_spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {ReflectorHost} from '@angular/language-service/src/reflector_host';
1313
import * as ts from 'typescript';
1414

1515
import {Symbol, SymbolQuery, SymbolTable} from '../../src/diagnostics/symbols';
16-
import {getSymbolQuery} from '../../src/diagnostics/typescript_symbols';
16+
import {getSymbolQuery, toSymbolTableFactory} from '../../src/diagnostics/typescript_symbols';
1717
import {CompilerOptions} from '../../src/transformers/api';
1818
import {Directory} from '../mocks';
1919

@@ -57,6 +57,19 @@ describe('symbol query', () => {
5757
});
5858
});
5959

60+
describe('toSymbolTableFactory(tsVersion)', () => {
61+
it('should return a Map for versions of TypeScript >= 2.2 and a dictionary otherwise', () => {
62+
const a = { name: 'a' } as ts.Symbol;
63+
const b = { name: 'b' } as ts.Symbol;
64+
65+
expect(toSymbolTableFactory('2.1')([a, b]) instanceof Map).toEqual(false);
66+
expect(toSymbolTableFactory('2.4')([a, b]) instanceof Map).toEqual(true);
67+
68+
// Check that for the lower bound version `2.2`, toSymbolTableFactory('2.2') returns a map
69+
expect(toSymbolTableFactory('2.2')([a, b]) instanceof Map).toEqual(true);
70+
});
71+
});
72+
6073
function appComponentSource(template: string): string {
6174
return `
6275
import {Component} from '@angular/core';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {compareNumbers, compareVersions, isVersionBetween, toNumbers} from '../../src/diagnostics/typescript_version';
10+
11+
12+
describe('toNumbers', () => {
13+
it('should handle strings', () => {
14+
expect(toNumbers('2')).toEqual([2]);
15+
expect(toNumbers('2.1')).toEqual([2, 1]);
16+
expect(toNumbers('2.0.1')).toEqual([2, 0, 1]);
17+
});
18+
});
19+
20+
describe('compareNumbers', () => {
21+
22+
it('should handle empty arrays', () => { expect(compareNumbers([], [])).toEqual(0); });
23+
24+
it('should handle arrays of same length', () => {
25+
expect(compareNumbers([1], [3])).toEqual(-1);
26+
expect(compareNumbers([3], [1])).toEqual(1);
27+
28+
expect(compareNumbers([1, 0], [1, 0])).toEqual(0);
29+
30+
expect(compareNumbers([1, 1], [1, 0])).toEqual(1);
31+
expect(compareNumbers([1, 0], [1, 1])).toEqual(-1);
32+
33+
expect(compareNumbers([1, 0, 9], [1, 1, 0])).toEqual(-1);
34+
expect(compareNumbers([1, 1, 0], [1, 0, 9])).toEqual(1);
35+
});
36+
37+
it('should handle arrays of different length', () => {
38+
expect(compareNumbers([2], [2, 1])).toEqual(-1);
39+
expect(compareNumbers([2, 1], [2])).toEqual(1);
40+
41+
expect(compareNumbers([0, 9], [1])).toEqual(-1);
42+
expect(compareNumbers([1], [0, 9])).toEqual(1);
43+
44+
expect(compareNumbers([2], [])).toEqual(1);
45+
expect(compareNumbers([], [2])).toEqual(-1);
46+
47+
expect(compareNumbers([1, 0], [1, 0, 0, 0])).toEqual(0);
48+
});
49+
});
50+
51+
describe('isVersionBetween', () => {
52+
it('should correctly check if a typescript version is within a given range', () => {
53+
expect(isVersionBetween('2.7.0', '2.40')).toEqual(false);
54+
expect(isVersionBetween('2.40', '2.7.0')).toEqual(true);
55+
56+
expect(isVersionBetween('2.7.2', '2.7.0', '2.8.0')).toEqual(true);
57+
58+
expect(isVersionBetween('2.7.2', '2.7.7', '2.8.0')).toEqual(false);
59+
});
60+
});
61+
62+
describe('compareVersions', () => {
63+
it('should correctly compare versions', () => {
64+
expect(compareVersions('2.7.0', '2.40')).toEqual(-1);
65+
expect(compareVersions('2.40', '2.7.0')).toEqual(1);
66+
expect(compareVersions('2.40', '2.40')).toEqual(0);
67+
expect(compareVersions('2.40', '2.41')).toEqual(-1);
68+
expect(compareVersions('2', '2.1')).toEqual(-1);
69+
});
70+
});

packages/compiler-cli/test/transformers/program_spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as ts from 'typescript';
1313

1414
import {formatDiagnostics} from '../../src/perform_compile';
1515
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
16-
import {createSrcToOutPathMapper} from '../../src/transformers/program';
16+
import {checkVersion, createSrcToOutPathMapper} from '../../src/transformers/program';
1717
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
1818
import {TestSupport, expectNoDiagnosticsInProgram, isInBazel, setup} from '../test_support';
1919

@@ -1048,4 +1048,35 @@ describe('ng program', () => {
10481048
.toContain('Function expressions are not supported');
10491049
});
10501050
});
1051+
1052+
describe('checkVersion', () => {
1053+
const MIN_TS_VERSION = '2.7.2';
1054+
const MAX_TS_VERSION = '2.8.0';
1055+
1056+
const versionError = (version: string) =>
1057+
`The Angular Compiler requires TypeScript >=${MIN_TS_VERSION} and <${MAX_TS_VERSION} but ${version} was found instead.`;
1058+
1059+
it('should not throw when a supported TypeScript version is used', () => {
1060+
expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, undefined)).not.toThrow();
1061+
expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, false)).not.toThrow();
1062+
expect(() => checkVersion('2.7.2', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow();
1063+
});
1064+
1065+
it('should handle a TypeScript version < the minimum supported one', () => {
1066+
expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, undefined))
1067+
.toThrowError(versionError('2.4.1'));
1068+
expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, false))
1069+
.toThrowError(versionError('2.4.1'));
1070+
expect(() => checkVersion('2.4.1', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow();
1071+
});
1072+
1073+
it('should handle a TypeScript version > the maximum supported one', () => {
1074+
expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, undefined))
1075+
.toThrowError(versionError('2.9.0'));
1076+
expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, false))
1077+
.toThrowError(versionError('2.9.0'));
1078+
expect(() => checkVersion('2.9.0', MIN_TS_VERSION, MAX_TS_VERSION, true)).not.toThrow();
1079+
});
1080+
});
1081+
10511082
});

0 commit comments

Comments
 (0)