2018-07-16 08:51:14 +01:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2018-07-16 08:51:14 +01:00
|
|
|
*
|
|
|
|
* 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';
|
2020-04-06 08:30:08 +01:00
|
|
|
|
2020-12-30 17:03:41 +00:00
|
|
|
import {absoluteFrom, AbsoluteFsPath, isRooted, ReadonlyFileSystem} from '../../src/ngtsc/file_system';
|
2020-09-29 20:42:20 +01:00
|
|
|
import {DeclarationNode, KnownDeclaration} from '../../src/ngtsc/reflection';
|
2018-07-16 08:51:14 +01:00
|
|
|
|
2019-08-27 17:36:25 +03:00
|
|
|
/**
|
|
|
|
* A list (`Array`) of partially ordered `T` items.
|
|
|
|
*
|
|
|
|
* The items in the list are partially ordered in the sense that any element has either the same or
|
|
|
|
* higher precedence than any element which appears later in the list. What "higher precedence"
|
|
|
|
* means and how it is determined is implementation-dependent.
|
|
|
|
*
|
|
|
|
* See [PartiallyOrderedSet](https://en.wikipedia.org/wiki/Partially_ordered_set) for more details.
|
|
|
|
* (Refraining from using the term "set" here, to avoid confusion with JavaScript's
|
|
|
|
* [Set](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set).)
|
|
|
|
*
|
|
|
|
* NOTE: A plain `Array<T>` is not assignable to a `PartiallyOrderedList<T>`, but a
|
|
|
|
* `PartiallyOrderedList<T>` is assignable to an `Array<T>`.
|
|
|
|
*/
|
|
|
|
export interface PartiallyOrderedList<T> extends Array<T> {
|
|
|
|
_partiallyOrdered: true;
|
|
|
|
|
|
|
|
map<U>(callbackfn: (value: T, index: number, array: PartiallyOrderedList<T>) => U, thisArg?: any):
|
|
|
|
PartiallyOrderedList<U>;
|
|
|
|
slice(...args: Parameters<Array<T>['slice']>): PartiallyOrderedList<T>;
|
|
|
|
}
|
|
|
|
|
2018-07-16 08:51:14 +01:00
|
|
|
export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) => ts.Symbol {
|
|
|
|
return function(symbol: ts.Symbol) {
|
|
|
|
return ts.SymbolFlags.Alias & symbol.flags ? checker.getAliasedSymbol(symbol) : symbol;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-06 08:30:08 +01:00
|
|
|
export function isDefined<T>(value: T|undefined|null): value is T {
|
2018-07-30 15:23:22 +03:00
|
|
|
return (value !== undefined) && (value !== null);
|
2018-07-16 08:51:14 +01:00
|
|
|
}
|
|
|
|
|
2020-04-06 08:30:08 +01:00
|
|
|
export function getNameText(name: ts.PropertyName|ts.BindingName): string {
|
2018-07-16 08:51:14 +01:00
|
|
|
return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText();
|
2018-07-30 15:23:22 +03:00
|
|
|
}
|
2018-08-17 07:50:55 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse down the AST and capture all the nodes that satisfy the test.
|
|
|
|
* @param node The start node.
|
|
|
|
* @param test The function that tests whether a node should be included.
|
|
|
|
* @returns a collection of nodes that satisfy the test.
|
|
|
|
*/
|
|
|
|
export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T[] {
|
|
|
|
const nodes: T[] = [];
|
|
|
|
findAllVisitor(node);
|
|
|
|
return nodes;
|
|
|
|
|
|
|
|
function findAllVisitor(n: ts.Node) {
|
|
|
|
if (test(n)) {
|
|
|
|
nodes.push(n);
|
|
|
|
} else {
|
|
|
|
n.forEachChild(child => findAllVisitor(child));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-11-13 14:40:54 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the given declaration have a name which is an identifier?
|
|
|
|
* @param declaration The declaration to test.
|
2019-03-05 23:29:28 +01:00
|
|
|
* @returns true if the declaration has an identifier for a name.
|
2018-11-13 14:40:54 +00:00
|
|
|
*/
|
2020-09-29 20:42:20 +01:00
|
|
|
export function hasNameIdentifier(declaration: ts.Node): declaration is DeclarationNode&
|
2018-11-13 14:40:54 +00:00
|
|
|
{name: ts.Identifier} {
|
2020-09-27 11:42:43 +01:00
|
|
|
const namedDeclaration: ts.Node&{name?: ts.Node} = declaration;
|
2019-04-01 14:20:34 -07:00
|
|
|
return namedDeclaration.name !== undefined && ts.isIdentifier(namedDeclaration.name);
|
2018-11-13 14:40:54 +00:00
|
|
|
}
|
2019-04-28 20:47:57 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Test whether a path is "relative".
|
|
|
|
*
|
2020-04-01 17:31:04 +03:00
|
|
|
* Relative paths start with `/`, `./` or `../` (or the Windows equivalents); or are simply `.` or
|
|
|
|
* `..`.
|
2019-04-28 20:47:57 +01:00
|
|
|
*/
|
|
|
|
export function isRelativePath(path: string): boolean {
|
2020-04-01 17:31:04 +03:00
|
|
|
return isRooted(path) || /^\.\.?(\/|\\|$)/.test(path);
|
2019-04-28 20:47:57 +01:00
|
|
|
}
|
2019-07-11 12:34:45 +01:00
|
|
|
|
2019-12-25 02:10:01 +02:00
|
|
|
/**
|
|
|
|
* A `Map`-like object that can compute and memoize a missing value for any key.
|
|
|
|
*
|
|
|
|
* The computed values are memoized, so the factory function is not called more than once per key.
|
|
|
|
* This is useful for storing values that are expensive to compute and may be used multiple times.
|
|
|
|
*/
|
|
|
|
// NOTE:
|
|
|
|
// Ideally, this class should extend `Map`, but that causes errors in ES5 transpiled code:
|
|
|
|
// `TypeError: Constructor Map requires 'new'`
|
|
|
|
export class FactoryMap<K, V> {
|
|
|
|
private internalMap: Map<K, V>;
|
|
|
|
|
|
|
|
constructor(private factory: (key: K) => V, entries?: readonly(readonly[K, V])[]|null) {
|
|
|
|
this.internalMap = new Map(entries);
|
|
|
|
}
|
|
|
|
|
|
|
|
get(key: K): V {
|
|
|
|
if (!this.internalMap.has(key)) {
|
|
|
|
this.internalMap.set(key, this.factory(key));
|
|
|
|
}
|
|
|
|
|
2020-04-06 08:30:08 +01:00
|
|
|
return this.internalMap.get(key)!;
|
2019-12-25 02:10:01 +02:00
|
|
|
}
|
|
|
|
|
2020-04-06 08:30:08 +01:00
|
|
|
set(key: K, value: V): void {
|
|
|
|
this.internalMap.set(key, value);
|
|
|
|
}
|
2019-12-25 02:10:01 +02:00
|
|
|
}
|
|
|
|
|
2019-07-11 12:34:45 +01:00
|
|
|
/**
|
|
|
|
* Attempt to resolve a `path` to a file by appending the provided `postFixes`
|
|
|
|
* to the `path` and checking if the file exists on disk.
|
|
|
|
* @returns An absolute path to the first matching existing file, or `null` if none exist.
|
|
|
|
*/
|
|
|
|
export function resolveFileWithPostfixes(
|
2020-12-30 17:03:41 +00:00
|
|
|
fs: ReadonlyFileSystem, path: AbsoluteFsPath, postFixes: string[]): AbsoluteFsPath|null {
|
2019-07-11 12:34:45 +01:00
|
|
|
for (const postFix of postFixes) {
|
|
|
|
const testPath = absoluteFrom(path + postFix);
|
|
|
|
if (fs.exists(testPath) && fs.stat(testPath).isFile()) {
|
|
|
|
return testPath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2019-07-19 23:22:52 +02:00
|
|
|
|
fix(ngcc): correctly detect emitted TS helpers in ES5 (#35191)
In ES5 code, TypeScript requires certain helpers (such as
`__spreadArrays()`) to be able to support ES2015+ features. These
helpers can be either imported from `tslib` (by setting the
`importHelpers` TS compiler option to `true`) or emitted inline (by
setting the `importHelpers` and `noEmitHelpers` TS compiler options to
`false`, which is the default value for both).
Ngtsc's `StaticInterpreter` (which is also used during ngcc processing)
is able to statically evaluate some of these helpers (currently
`__assign()`, `__spread()` and `__spreadArrays()`), as long as
`ReflectionHost#getDefinitionOfFunction()` correctly detects the
declaration of the helper. For this to happen, the left-hand side of the
corresponding call expression (i.e. `__spread(...)` or
`tslib.__spread(...)`) must be evaluated as a function declaration for
`getDefinitionOfFunction()` to be called with.
In the case of imported helpers, the `tslib.__someHelper` expression was
resolved to a function declaration of the form
`export declare function __someHelper(...args: any[][]): any[];`, which
allows `getDefinitionOfFunction()` to correctly map it to a TS helper.
In contrast, in the case of emitted helpers (and regardless of the
module format: `CommonJS`, `ESNext`, `UMD`, etc.)), the `__someHelper`
identifier was resolved to a variable declaration of the form
`var __someHelper = (this && this.__someHelper) || function () { ... }`,
which upon further evaluation was categorized as a `DynamicValue`
(prohibiting further evaluation by the `getDefinitionOfFunction()`).
As a result of the above, emitted TypeScript helpers were not evaluated
in ES5 code.
---
This commit changes the detection of TS helpers to leverage the existing
`KnownFn` feature (previously only used for built-in functions).
`Esm5ReflectionHost` is changed to always return `KnownDeclaration`s for
TS helpers, both imported (`getExportsOfModule()`) as well as emitted
(`getDeclarationOfIdentifier()`).
Similar changes are made to `CommonJsReflectionHost` and
`UmdReflectionHost`.
The `KnownDeclaration`s are then mapped to `KnownFn`s in
`StaticInterpreter`, allowing it to statically evaluate call expressions
involving any kind of TS helpers.
Jira issue: https://angular-team.atlassian.net/browse/FW-1689
PR Close #35191
2020-02-06 18:44:49 +02:00
|
|
|
/**
|
|
|
|
* Determine whether a function declaration corresponds with a TypeScript helper function, returning
|
|
|
|
* its kind if so or null if the declaration does not seem to correspond with such a helper.
|
|
|
|
*/
|
2020-09-29 20:42:20 +01:00
|
|
|
export function getTsHelperFnFromDeclaration(decl: DeclarationNode): KnownDeclaration|null {
|
fix(ngcc): correctly detect emitted TS helpers in ES5 (#35191)
In ES5 code, TypeScript requires certain helpers (such as
`__spreadArrays()`) to be able to support ES2015+ features. These
helpers can be either imported from `tslib` (by setting the
`importHelpers` TS compiler option to `true`) or emitted inline (by
setting the `importHelpers` and `noEmitHelpers` TS compiler options to
`false`, which is the default value for both).
Ngtsc's `StaticInterpreter` (which is also used during ngcc processing)
is able to statically evaluate some of these helpers (currently
`__assign()`, `__spread()` and `__spreadArrays()`), as long as
`ReflectionHost#getDefinitionOfFunction()` correctly detects the
declaration of the helper. For this to happen, the left-hand side of the
corresponding call expression (i.e. `__spread(...)` or
`tslib.__spread(...)`) must be evaluated as a function declaration for
`getDefinitionOfFunction()` to be called with.
In the case of imported helpers, the `tslib.__someHelper` expression was
resolved to a function declaration of the form
`export declare function __someHelper(...args: any[][]): any[];`, which
allows `getDefinitionOfFunction()` to correctly map it to a TS helper.
In contrast, in the case of emitted helpers (and regardless of the
module format: `CommonJS`, `ESNext`, `UMD`, etc.)), the `__someHelper`
identifier was resolved to a variable declaration of the form
`var __someHelper = (this && this.__someHelper) || function () { ... }`,
which upon further evaluation was categorized as a `DynamicValue`
(prohibiting further evaluation by the `getDefinitionOfFunction()`).
As a result of the above, emitted TypeScript helpers were not evaluated
in ES5 code.
---
This commit changes the detection of TS helpers to leverage the existing
`KnownFn` feature (previously only used for built-in functions).
`Esm5ReflectionHost` is changed to always return `KnownDeclaration`s for
TS helpers, both imported (`getExportsOfModule()`) as well as emitted
(`getDeclarationOfIdentifier()`).
Similar changes are made to `CommonJsReflectionHost` and
`UmdReflectionHost`.
The `KnownDeclaration`s are then mapped to `KnownFn`s in
`StaticInterpreter`, allowing it to statically evaluate call expressions
involving any kind of TS helpers.
Jira issue: https://angular-team.atlassian.net/browse/FW-1689
PR Close #35191
2020-02-06 18:44:49 +02:00
|
|
|
if (!ts.isFunctionDeclaration(decl) && !ts.isVariableDeclaration(decl)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (decl.name === undefined || !ts.isIdentifier(decl.name)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return getTsHelperFnFromIdentifier(decl.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine whether an identifier corresponds with a TypeScript helper function (based on its
|
|
|
|
* name), returning its kind if so or null if the identifier does not seem to correspond with such a
|
|
|
|
* helper.
|
|
|
|
*/
|
|
|
|
export function getTsHelperFnFromIdentifier(id: ts.Identifier): KnownDeclaration|null {
|
|
|
|
switch (stripDollarSuffix(id.text)) {
|
|
|
|
case '__assign':
|
|
|
|
return KnownDeclaration.TsHelperAssign;
|
|
|
|
case '__spread':
|
|
|
|
return KnownDeclaration.TsHelperSpread;
|
|
|
|
case '__spreadArrays':
|
|
|
|
return KnownDeclaration.TsHelperSpreadArrays;
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-19 23:22:52 +02:00
|
|
|
/**
|
|
|
|
* An identifier may become repeated when bundling multiple source files into a single bundle, so
|
|
|
|
* bundlers have a strategy of suffixing non-unique identifiers with a suffix like $2. This function
|
|
|
|
* strips off such suffixes, so that ngcc deals with the canonical name of an identifier.
|
|
|
|
* @param value The value to strip any suffix of, if applicable.
|
|
|
|
* @returns The canonical representation of the value, without any suffix.
|
|
|
|
*/
|
|
|
|
export function stripDollarSuffix(value: string): string {
|
|
|
|
return value.replace(/\$\d+$/, '');
|
|
|
|
}
|
2019-12-18 14:03:05 +00:00
|
|
|
|
|
|
|
export function stripExtension(fileName: string): string {
|
|
|
|
return fileName.replace(/\..+$/, '');
|
|
|
|
}
|