From b7fbb18e4961e55be3e7a079fc334c62363aacf7 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 9 Feb 2024 14:22:55 +0000 Subject: [PATCH 1/3] feat: support signal inputs, queries and model --- .gitignore | 3 + .../__tests__/signal-inputs.spec.ts | 27 + package.json | 5 +- src/compiler/ng-jest-compiler.ts | 8 +- src/ngtsc/reflection/index.ts | 12 - src/ngtsc/reflection/src/host.ts | 869 ------------------ src/ngtsc/reflection/src/type_to_value.ts | 283 ------ src/ngtsc/reflection/src/typescript.ts | 706 -------------- src/ngtsc/reflection/src/util.ts | 29 - src/ngtsc/ts_compatibility/index.ts | 9 - .../src/ts_cross_version_utils.ts | 352 ------- src/transformers/downlevel-ctor.ts | 12 - .../downlevel_decorators_transform.ts | 627 ------------- .../downlevel_decorators_transform/index.ts | 9 - .../patch_alias_reference_resolution.ts | 137 --- src/transformers/esm_interop_inject.cjs | 7 + src/transformers/jit_transform.d.ts | 10 + src/transformers/replace-resources.ts | 5 +- tsconfig.build.json | 2 +- 19 files changed, 58 insertions(+), 3054 deletions(-) create mode 100644 e2e/ast-transformers/ng-jit-transformers/__tests__/signal-inputs.spec.ts delete mode 100644 src/ngtsc/reflection/index.ts delete mode 100644 src/ngtsc/reflection/src/host.ts delete mode 100644 src/ngtsc/reflection/src/type_to_value.ts delete mode 100644 src/ngtsc/reflection/src/typescript.ts delete mode 100644 src/ngtsc/reflection/src/util.ts delete mode 100644 src/ngtsc/ts_compatibility/index.ts delete mode 100644 src/ngtsc/ts_compatibility/src/ts_cross_version_utils.ts delete mode 100644 src/transformers/downlevel-ctor.ts delete mode 100644 src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts delete mode 100644 src/transformers/downlevel_decorators_transform/index.ts delete mode 100644 src/transformers/downlevel_decorators_transform/patch_alias_reference_resolution.ts create mode 100644 src/transformers/esm_interop_inject.cjs create mode 100644 src/transformers/jit_transform.d.ts diff --git a/.gitignore b/.gitignore index b296df8d53..ba3689eaa9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ website/.yarn/* !.yarn/versions !/e2e/full-ivy-lib/node_modules !/e2e/partial-ivy-lib/node_modules + +# Generated by `yarn build-transformers-bundle` automatically. +src/transformers/jit_transform.js diff --git a/e2e/ast-transformers/ng-jit-transformers/__tests__/signal-inputs.spec.ts b/e2e/ast-transformers/ng-jit-transformers/__tests__/signal-inputs.spec.ts new file mode 100644 index 0000000000..dfa8b2f916 --- /dev/null +++ b/e2e/ast-transformers/ng-jit-transformers/__tests__/signal-inputs.spec.ts @@ -0,0 +1,27 @@ +import { Component, input } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +test('signal inputs', () => { + @Component({ + selector: 'greet', + standalone: true, + template: 'Name: {{name()}}', + }) + class GreetCmp { + name = input.required(); + } + + @Component({ + standalone: true, + imports: [GreetCmp], + template: '', + }) + class TestCmp { + name = 'John'; + } + + const fixture = TestBed.createComponent(TestCmp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Name: John'); +}); diff --git a/package.json b/package.json index c2aa7ec6c4..cf8e16f569 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,14 @@ "testing" ], "scripts": { - "build": "tsc -p tsconfig.build.json", + "build-transformers-bundle": "esbuild --bundle --minify src/transformers/jit_transform.d.ts --platform=node --outfile=./src/transformers/jit_transform.js --format=cjs --define:import.meta.url=import_meta_url --inject:./src/transformers/esm_interop_inject.cjs && cp src/transformers/jit_transform.js build/transformers/jit_transform.js", + "build": "tsc -p tsconfig.build.json && yarn build-transformers-bundle", "lint": "eslint --ext .js,.ts .", "lint-fix": "eslint --fix --ext .js,.ts .", "lint-prettier": "prettier \"**/*.{yml,yaml,md}\" --write", "lint-prettier-ci": "prettier \"**/*.{yml,yaml,md}\" --check", "pretest": "tsc -p tsconfig.spec.json --noEmit", - "test": "jest", + "test": "yarn build && jest", "test-examples": "node scripts/test-examples.js", "doc": "cd website && yarn start", "doc:build": "cd website && yarn build", diff --git a/src/compiler/ng-jest-compiler.ts b/src/compiler/ng-jest-compiler.ts index 33807954e5..d8a4d6f3e6 100644 --- a/src/compiler/ng-jest-compiler.ts +++ b/src/compiler/ng-jest-compiler.ts @@ -1,7 +1,7 @@ import { type TsJestAstTransformer, TsCompiler, type ConfigSet } from 'ts-jest'; import type * as ts from 'typescript'; -import { constructorParametersDownlevelTransform } from '../transformers/downlevel-ctor'; +import { angularJitApplicationTransform } from '../transformers/jit_transform'; import { replaceResources } from '../transformers/replace-resources'; export class NgJestCompiler extends TsCompiler { @@ -112,6 +112,9 @@ export class NgJestCompiler extends TsCompiler { } protected _makeTransformers(customTransformers: TsJestAstTransformer): ts.CustomTransformers { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const program = this.program!; + return { ...super._makeTransformers(customTransformers).after, ...super._makeTransformers(customTransformers).afterDeclarations, @@ -120,8 +123,7 @@ export class NgJestCompiler extends TsCompiler { beforeTransformer.factory(this, beforeTransformer.options), ), replaceResources(this), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - constructorParametersDownlevelTransform(this.program!), + angularJitApplicationTransform(program), ] as Array | ts.CustomTransformerFactory>, }; } diff --git a/src/ngtsc/reflection/index.ts b/src/ngtsc/reflection/index.ts deleted file mode 100644 index 0cd1eae59d..0000000000 --- a/src/ngtsc/reflection/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 './src/host'; -export {typeNodeToValueExpr} from './src/type_to_value'; -export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/typescript'; -export {isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from './src/util'; diff --git a/src/ngtsc/reflection/src/host.ts b/src/ngtsc/reflection/src/host.ts deleted file mode 100644 index 632eb060df..0000000000 --- a/src/ngtsc/reflection/src/host.ts +++ /dev/null @@ -1,869 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** - * Metadata extracted from an instance of a decorator on another declaration, or synthesized from - * other information about a class. - */ -export type Decorator = ConcreteDecorator|SyntheticDecorator; - -export interface BaseDecorator { - /** - * Name by which the decorator was invoked in the user's code. - * - * This is distinct from the name by which the decorator was imported (though in practice they - * will usually be the same). - */ - name: string; - - /** - * Identifier which refers to the decorator in the user's code. - */ - identifier: DecoratorIdentifier|null; - - /** - * `Import` by which the decorator was brought into the module in which it was invoked, or `null` - * if the decorator was declared in the same module and not imported. - */ - import: Import|null; - - /** - * TypeScript reference to the decorator itself, or `null` if the decorator is synthesized (e.g. - * in ngcc). - */ - node: ts.Node|null; - - /** - * Arguments of the invocation of the decorator, if the decorator is invoked, or `null` - * otherwise. - */ - args: ts.Expression[]|null; -} - -/** - * Metadata extracted from an instance of a decorator on another declaration, which was actually - * present in a file. - * - * Concrete decorators always have an `identifier` and a `node`. - */ -export interface ConcreteDecorator extends BaseDecorator { - identifier: DecoratorIdentifier; - node: ts.Node; -} - -/** - * Synthetic decorators never have an `identifier` or a `node`, but know the node for which they - * were synthesized. - */ -export interface SyntheticDecorator extends BaseDecorator { - identifier: null; - node: null; - - /** - * The `ts.Node` for which this decorator was created. - */ - synthesizedFor: ts.Node; -} - -export const Decorator = { - nodeForError: (decorator: Decorator): ts.Node => { - if (decorator.node !== null) { - return decorator.node; - } else { - // TODO(alxhub): we can't rely on narrowing until TS 3.6 is in g3. - return (decorator as SyntheticDecorator).synthesizedFor; - } - }, -}; - -/** - * A decorator is identified by either a simple identifier (e.g. `Decorator`) or, in some cases, - * a namespaced property access (e.g. `core.Decorator`). - */ -export type DecoratorIdentifier = ts.Identifier|NamespacedIdentifier; -export type NamespacedIdentifier = ts.PropertyAccessExpression&{ - expression: ts.Identifier; - name: ts.Identifier -}; -export function isDecoratorIdentifier(exp: ts.Expression): exp is DecoratorIdentifier { - return ts.isIdentifier(exp) || - ts.isPropertyAccessExpression(exp) && ts.isIdentifier(exp.expression) && - ts.isIdentifier(exp.name); -} - -/** - * The `ts.Declaration` of a "class". - * - * Classes are represented differently in different code formats: - * - In TS code, they are typically defined using the `class` keyword. - * - In ES2015 code, they are usually defined using the `class` keyword, but they can also be - * variable declarations, which are initialized to a class expression (e.g. - * `let Foo = Foo1 = class Foo {}`). - * - In ES5 code, they are typically defined as variable declarations being assigned the return - * value of an IIFE. The actual "class" is implemented as a constructor function inside the IIFE, - * but the outer variable declaration represents the "class" to the rest of the program. - * - * For `ReflectionHost` purposes, a class declaration should always have a `name` identifier, - * because we need to be able to reference it in other parts of the program. - */ -export type ClassDeclaration = T&{name: ts.Identifier}; - -/** - * An enumeration of possible kinds of class members. - */ -export enum ClassMemberKind { - Constructor, - Getter, - Setter, - Property, - Method, -} - -/** - * A member of a class, such as a property, method, or constructor. - */ -export interface ClassMember { - /** - * TypeScript reference to the class member itself, or null if it is not applicable. - */ - node: ts.Node|null; - - /** - * Indication of which type of member this is (property, method, etc). - */ - kind: ClassMemberKind; - - /** - * TypeScript `ts.TypeNode` representing the type of the member, or `null` if not present or - * applicable. - */ - type: ts.TypeNode|null; - - /** - * Name of the class member. - */ - name: string; - - /** - * TypeScript `ts.Identifier` or `ts.StringLiteral` representing the name of the member, or `null` - * if no such node is present. - * - * The `nameNode` is useful in writing references to this member that will be correctly source- - * mapped back to the original file. - */ - nameNode: ts.Identifier|ts.StringLiteral|null; - - /** - * TypeScript `ts.Expression` which represents the value of the member. - * - * If the member is a property, this will be the property initializer if there is one, or null - * otherwise. - */ - value: ts.Expression|null; - - /** - * TypeScript `ts.Declaration` which represents the implementation of the member. - * - * In TypeScript code this is identical to the node, but in downleveled code this should always be - * the Declaration which actually represents the member's runtime value. - * - * For example, the TS code: - * - * ``` - * class Clazz { - * static get property(): string { - * return 'value'; - * } - * } - * ``` - * - * Downlevels to: - * - * ``` - * var Clazz = (function () { - * function Clazz() { - * } - * Object.defineProperty(Clazz, "property", { - * get: function () { - * return 'value'; - * }, - * enumerable: true, - * configurable: true - * }); - * return Clazz; - * }()); - * ``` - * - * In this example, for the property "property", the node would be the entire - * Object.defineProperty ExpressionStatement, but the implementation would be this - * FunctionDeclaration: - * - * ``` - * function () { - * return 'value'; - * }, - * ``` - */ - implementation: ts.Declaration|null; - - /** - * Whether the member is static or not. - */ - isStatic: boolean; - - /** - * Any `Decorator`s which are present on the member, or `null` if none are present. - */ - decorators: Decorator[]|null; -} - -export const enum TypeValueReferenceKind { - LOCAL, - IMPORTED, - UNAVAILABLE, -} - -/** - * A type reference that refers to any type via a `ts.Expression` that's valid within the local file - * where the type was referenced. - */ -export interface LocalTypeValueReference { - kind: TypeValueReferenceKind.LOCAL; - - /** - * The synthesized expression to reference the type in a value position. - */ - expression: ts.Expression; - - /** - * If the type originates from a default import, the import statement is captured here to be able - * to track its usages, preventing the import from being elided if it was originally only used in - * a type-position. See `DefaultImportTracker` for details. - */ - defaultImportStatement: ts.ImportDeclaration|null; -} - -/** - * A reference that refers to a type that was imported, and gives the symbol `name` and the - * `moduleName` of the import. Note that this `moduleName` may be a relative path, and thus is - * likely only valid within the context of the file which contained the original type reference. - */ -export interface ImportedTypeValueReference { - kind: TypeValueReferenceKind.IMPORTED; - - /** - * The module specifier from which the `importedName` symbol should be imported. - */ - moduleName: string; - - /** - * The name of the top-level symbol that is imported from `moduleName`. If `nestedPath` is also - * present, a nested object is being referenced from the top-level symbol. - */ - importedName: string; - - /** - * If present, represents the symbol names that are referenced from the top-level import. - * When `null` or empty, the `importedName` itself is the symbol being referenced. - */ - nestedPath: string[]|null; - - valueDeclaration: DeclarationNode; -} - -/** - * A representation for a type value reference that is used when no value is available. This can - * occur due to various reasons, which is indicated in the `reason` field. - */ -export interface UnavailableTypeValueReference { - kind: TypeValueReferenceKind.UNAVAILABLE; - - /** - * The reason why no value reference could be determined for a type. - */ - reason: UnavailableValue; -} - -/** - * The various reasons why the compiler may be unable to synthesize a value from a type reference. - */ -export const enum ValueUnavailableKind { - /** - * No type node was available. - */ - MISSING_TYPE, - - /** - * The type does not have a value declaration, e.g. an interface. - */ - NO_VALUE_DECLARATION, - - /** - * The type is imported using a type-only imports, so it is not suitable to be used in a - * value-position. - */ - TYPE_ONLY_IMPORT, - - /** - * The type reference could not be resolved to a declaration. - */ - UNKNOWN_REFERENCE, - - /** - * The type corresponds with a namespace. - */ - NAMESPACE, - - /** - * The type is not supported in the compiler, for example union types. - */ - UNSUPPORTED, -} - - -export interface UnsupportedType { - kind: ValueUnavailableKind.UNSUPPORTED; - typeNode: ts.TypeNode; -} - -export interface NoValueDeclaration { - kind: ValueUnavailableKind.NO_VALUE_DECLARATION; - typeNode: ts.TypeNode; - decl: ts.Declaration|null; -} - -export interface TypeOnlyImport { - kind: ValueUnavailableKind.TYPE_ONLY_IMPORT; - typeNode: ts.TypeNode; - node: ts.ImportClause|ts.ImportSpecifier; -} - -export interface NamespaceImport { - kind: ValueUnavailableKind.NAMESPACE; - typeNode: ts.TypeNode; - importClause: ts.ImportClause; -} - -export interface UnknownReference { - kind: ValueUnavailableKind.UNKNOWN_REFERENCE; - typeNode: ts.TypeNode; -} - -export interface MissingType { - kind: ValueUnavailableKind.MISSING_TYPE; -} - -/** - * The various reasons why a type node may not be referred to as a value. - */ -export type UnavailableValue = - UnsupportedType|NoValueDeclaration|TypeOnlyImport|NamespaceImport|UnknownReference|MissingType; - -/** - * A reference to a value that originated from a type position. - * - * For example, a constructor parameter could be declared as `foo: Foo`. A `TypeValueReference` - * extracted from this would refer to the value of the class `Foo` (assuming it was actually a - * type). - * - * See the individual types for additional information. - */ -export type TypeValueReference = - LocalTypeValueReference|ImportedTypeValueReference|UnavailableTypeValueReference; - -/** - * A parameter to a constructor. - */ -export interface CtorParameter { - /** - * Name of the parameter, if available. - * - * Some parameters don't have a simple string name (for example, parameters which are destructured - * into multiple variables). In these cases, `name` can be `null`. - */ - name: string|null; - - /** - * TypeScript `ts.BindingName` representing the name of the parameter. - * - * The `nameNode` is useful in writing references to this member that will be correctly source- - * mapped back to the original file. - */ - nameNode: ts.BindingName; - - /** - * Reference to the value of the parameter's type annotation, if it's possible to refer to the - * parameter's type as a value. - * - * This can either be a reference to a local value, a reference to an imported value, or no - * value if no is present or cannot be represented as an expression. - */ - typeValueReference: TypeValueReference; - - /** - * TypeScript `ts.TypeNode` representing the type node found in the type position. - * - * This field can be used for diagnostics reporting if `typeValueReference` is `null`. - * - * Can be null, if the param has no type declared. - */ - typeNode: ts.TypeNode|null; - - /** - * Any `Decorator`s which are present on the parameter, or `null` if none are present. - */ - decorators: Decorator[]|null; -} - -/** - * Definition of a function or method, including its body if present and any parameters. - * - * In TypeScript code this metadata will be a simple reflection of the declarations in the node - * itself. In ES5 code this can be more complicated, as the default values for parameters may - * be extracted from certain body statements. - */ -export interface FunctionDefinition { - /** - * A reference to the node which declares the function. - */ - node: ts.MethodDeclaration|ts.FunctionDeclaration|ts.FunctionExpression|ts.VariableDeclaration; - - /** - * Statements of the function body, if a body is present, or null if no body is present or the - * function is identified to represent a tslib helper function, in which case `helper` will - * indicate which helper this function represents. - * - * This list may have been filtered to exclude statements which perform parameter default value - * initialization. - */ - body: ts.Statement[]|null; - - /** - * Metadata regarding the function's parameters, including possible default value expressions. - */ - parameters: Parameter[]; -} - -/** - * Possible declarations of known values, such as built-in objects/functions or TypeScript helpers. - */ -export enum KnownDeclaration { - /** - * Indicates the JavaScript global `Object` class. - */ - JsGlobalObject, - - /** - * Indicates the `__assign` TypeScript helper function. - */ - TsHelperAssign, - - /** - * Indicates the `__spread` TypeScript helper function. - */ - TsHelperSpread, - - /** - * Indicates the `__spreadArrays` TypeScript helper function. - */ - TsHelperSpreadArrays, - - /** - * Indicates the `__spreadArray` TypeScript helper function. - */ - TsHelperSpreadArray, - - /** - * Indicates the `__read` TypeScript helper function. - */ - TsHelperRead, -} - -/** - * A parameter to a function or method. - */ -export interface Parameter { - /** - * Name of the parameter, if available. - */ - name: string|null; - - /** - * Declaration which created this parameter. - */ - node: ts.ParameterDeclaration; - - /** - * Expression which represents the default value of the parameter, if any. - */ - initializer: ts.Expression|null; -} - -/** - * The source of an imported symbol, including the original symbol name and the module from which it - * was imported. - */ -export interface Import { - /** - * The name of the imported symbol under which it was exported (not imported). - */ - name: string; - - /** - * The module from which the symbol was imported. - * - * This could either be an absolute module name (@angular/core for example) or a relative path. - */ - from: string; -} - -/** - * A single enum member extracted from JavaScript when no `ts.EnumDeclaration` is available. - */ -export interface EnumMember { - /** - * The name of the enum member. - */ - name: ts.PropertyName; - - /** - * The initializer expression of the enum member. Unlike in TypeScript, this is always available - * in emitted JavaScript. - */ - initializer: ts.Expression; -} - -/** - * A type that is used to identify a declaration. - * - * Declarations are normally `ts.Declaration` types such as variable declarations, class - * declarations, function declarations etc. - * But in some cases there is no `ts.Declaration` that can be used for a declaration, such - * as when they are declared inline as part of an exported expression. Then we must use a - * `ts.Expression` as the declaration. - * An example of this is `exports.someVar = 42` where the declaration expression would be - * `exports.someVar`. - */ -export type DeclarationNode = ts.Declaration|ts.Expression; - -/** - * The type of a Declaration - whether its node is concrete (ts.Declaration) or inline - * (ts.Expression). See `ConcreteDeclaration`, `InlineDeclaration` and `DeclarationNode` for more - * information about this. - */ -export const enum DeclarationKind { - Concrete, - Inline, -} - -/** - * Base type for all `Declaration`s. - */ -export interface BaseDeclaration { - /** - * The type of the underlying `node`. - */ - kind: DeclarationKind; - - /** - * The absolute module path from which the symbol was imported into the application, if the symbol - * was imported via an absolute module (even through a chain of re-exports). If the symbol is part - * of the application and was not imported from an absolute path, this will be `null`. - */ - viaModule: string|null; - - /** - * TypeScript reference to the declaration itself, if one exists. - */ - node: T; - - /** - * If set, describes the type of the known declaration this declaration resolves to. - */ - known: KnownDeclaration|null; -} - -/** - * Returns true if the `decl` is a `ConcreteDeclaration` (ie. that its `node` property is a - * `ts.Declaration`). - */ -export function isConcreteDeclaration(decl: Declaration): decl is ConcreteDeclaration { - return decl.kind === DeclarationKind.Concrete; -} - -export interface ConcreteDeclaration extends - BaseDeclaration { - kind: DeclarationKind.Concrete; - - /** - * Optionally represents a special identity of the declaration, or `null` if the declaration - * does not have a special identity. - */ - identity: SpecialDeclarationIdentity|null; -} - -export type SpecialDeclarationIdentity = DownleveledEnum; - -export const enum SpecialDeclarationKind { - DownleveledEnum, -} - -/** - * A special declaration identity that represents an enum. This is used in downleveled forms where - * a `ts.EnumDeclaration` is emitted in an alternative form, e.g. an IIFE call that declares all - * members. - */ -export interface DownleveledEnum { - kind: SpecialDeclarationKind.DownleveledEnum; - enumMembers: EnumMember[]; -} - -/** - * A declaration that does not have an associated TypeScript `ts.Declaration`. - * - * This can occur in some downlevelings when an `export const VAR = ...;` (a `ts.Declaration`) is - * transpiled to an assignment statement (e.g. `exports.VAR = ...;`). There is no `ts.Declaration` - * associated with `VAR` in that case, only an expression. - */ -export interface InlineDeclaration extends - BaseDeclaration> { - kind: DeclarationKind.Inline; - implementation?: DeclarationNode; -} - -/** - * The declaration of a symbol, along with information about how it was imported into the - * application. - */ -export type Declaration = - ConcreteDeclaration|InlineDeclaration; - -/** - * Abstracts reflection operations on a TypeScript AST. - * - * Depending on the format of the code being interpreted, different concepts are represented - * with different syntactical structures. The `ReflectionHost` abstracts over those differences and - * presents a single API by which the compiler can query specific information about the AST. - * - * All operations on the `ReflectionHost` require the use of TypeScript `ts.Node`s with binding - * information already available (that is, nodes that come from a `ts.Program` that has been - * type-checked, and are not synthetically created). - */ -export interface ReflectionHost { - /** - * Examine a declaration (for example, of a class or function) and return metadata about any - * decorators present on the declaration. - * - * @param declaration a TypeScript `ts.Declaration` node representing the class or function over - * which to reflect. For example, if the intent is to reflect the decorators of a class and the - * source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5 - * format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the - * result of an IIFE execution. - * - * @returns an array of `Decorator` metadata if decorators are present on the declaration, or - * `null` if either no decorators were present or if the declaration is not of a decoratable type. - */ - getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null; - - /** - * Examine a declaration which should be of a class, and return metadata about the members of the - * class. - * - * @param clazz a `ClassDeclaration` representing the class over which to reflect. - * - * @returns an array of `ClassMember` metadata representing the members of the class. - * - * @throws if `declaration` does not resolve to a class declaration. - */ - getMembersOfClass(clazz: ClassDeclaration): ClassMember[]; - - /** - * Reflect over the constructor of a class and return metadata about its parameters. - * - * This method only looks at the constructor of a class directly and not at any inherited - * constructors. - * - * @param clazz a `ClassDeclaration` representing the class over which to reflect. - * - * @returns an array of `Parameter` metadata representing the parameters of the constructor, if - * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. - * If the class has no constructor, this method returns `null`. - */ - getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null; - - /** - * Reflect over a function and return metadata about its parameters and body. - * - * Functions in TypeScript and ES5 code have different AST representations, in particular around - * default values for parameters. A TypeScript function has its default value as the initializer - * on the parameter declaration, whereas an ES5 function has its default value set in a statement - * of the form: - * - * if (param === void 0) { param = 3; } - * - * This method abstracts over these details, and interprets the function declaration and body to - * extract parameter default values and the "real" body. - * - * A current limitation is that this metadata has no representation for shorthand assignment of - * parameter objects in the function signature. - * - * @param fn a TypeScript `ts.Declaration` node representing the function over which to reflect. - * - * @returns a `FunctionDefinition` giving metadata about the function definition. - */ - getDefinitionOfFunction(fn: ts.Node): FunctionDefinition|null; - - /** - * Determine if an identifier was imported from another module and return `Import` metadata - * describing its origin. - * - * @param id a TypeScript `ts.Identifier` to reflect. - * - * @returns metadata about the `Import` if the identifier was imported from another module, or - * `null` if the identifier doesn't resolve to an import but instead is locally defined. - */ - getImportOfIdentifier(id: ts.Identifier): Import|null; - - /** - * Trace an identifier to its declaration, if possible. - * - * This method attempts to resolve the declaration of the given identifier, tracing back through - * imports and re-exports until the original declaration statement is found. A `Declaration` - * object is returned if the original declaration is found, or `null` is returned otherwise. - * - * If the declaration is in a different module, and that module is imported via an absolute path, - * this method also returns the absolute path of the imported module. For example, if the code is: - * - * ``` - * import {RouterModule} from '@angular/core'; - * - * export const ROUTES = RouterModule.forRoot([...]); - * ``` - * - * and if `getDeclarationOfIdentifier` is called on `RouterModule` in the `ROUTES` expression, - * then it would trace `RouterModule` via its import from `@angular/core`, and note that the - * definition was imported from `@angular/core` into the application where it was referenced. - * - * If the definition is re-exported several times from different absolute module names, only - * the first one (the one by which the application refers to the module) is returned. - * - * This module name is returned in the `viaModule` field of the `Declaration`. If The declaration - * is relative to the application itself and there was no import through an absolute path, then - * `viaModule` is `null`. - * - * @param id a TypeScript `ts.Identifier` to trace back to a declaration. - * - * @returns metadata about the `Declaration` if the original declaration is found, or `null` - * otherwise. - */ - getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null; - - /** - * Collect the declarations exported from a module by name. - * - * Iterates over the exports of a module (including re-exports) and returns a map of export - * name to its `Declaration`. If an exported value is itself re-exported from another module, - * the `Declaration`'s `viaModule` will reflect that. - * - * @param node a TypeScript `ts.Node` representing the module (for example a `ts.SourceFile`) for - * which to collect exports. - * - * @returns a map of `Declaration`s for the module's exports, by name. - */ - getExportsOfModule(module: ts.Node): Map|null; - - /** - * Check whether the given node actually represents a class. - */ - isClass(node: ts.Node): node is ClassDeclaration; - - /** - * Determines whether the given declaration, which should be a class, has a base class. - * - * @param clazz a `ClassDeclaration` representing the class over which to reflect. - */ - hasBaseClass(clazz: ClassDeclaration): boolean; - - /** - * Get an expression representing the base class (if any) of the given `clazz`. - * - * This expression is most commonly an Identifier, but is possible to inherit from a more dynamic - * expression. - * - * @param clazz the class whose base we want to get. - */ - getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null; - - /** - * Get the number of generic type parameters of a given class. - * - * @param clazz a `ClassDeclaration` representing the class over which to reflect. - * - * @returns the number of type parameters of the class, if known, or `null` if the declaration - * is not a class or has an unknown number of type parameters. - */ - getGenericArityOfClass(clazz: ClassDeclaration): number|null; - - /** - * Find the assigned value of a variable declaration. - * - * Normally this will be the initializer of the declaration, but where the variable is - * not a `const` we may need to look elsewhere for the variable's value. - * - * @param declaration a TypeScript variable declaration, whose value we want. - * @returns the value of the variable, as a TypeScript expression node, or `undefined` - * if the value cannot be computed. - */ - getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null; - - /** - * Take an exported declaration (maybe a class down-leveled to a variable) and look up the - * declaration of its type in a separate .d.ts tree. - * - * This function is allowed to return `null` if the current compilation unit does not have a - * separate .d.ts tree. When compiling TypeScript code this is always the case, since .d.ts files - * are produced only during the emit of such a compilation. When compiling .js code, however, - * there is frequently a parallel .d.ts tree which this method exposes. - * - * Note that the `ts.Declaration` returned from this function may not be from the same - * `ts.Program` as the input declaration. - */ - getDtsDeclaration(declaration: DeclarationNode): ts.Declaration|null; - - /** - * Get a `ts.Identifier` for a given `ClassDeclaration` which can be used to refer to the class - * within its definition (such as in static fields). - * - * This can differ from `clazz.name` when ngcc runs over ES5 code, since the class may have a - * different name within its IIFE wrapper than it does externally. - */ - getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier; - - /** - * Get a `ts.Identifier` for a given `ClassDeclaration` which can be used to refer to the class - * from statements that are "adjacent", and conceptually tightly bound, to the class but not - * actually inside it. - * - * Similar to `getInternalNameOfClass()`, this name can differ from `clazz.name` when ngcc runs - * over ES5 code, since these "adjacent" statements need to exist in the IIFE where the class may - * have a different name than it does externally. - */ - getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier; - - /** - * Returns `true` if a declaration is exported from the module in which it's defined. - * - * Not all mechanisms by which a declaration is exported can be statically detected, especially - * when processing already compiled JavaScript. A `false` result does not indicate that the - * declaration is never visible outside its module, only that it was not exported via one of the - * export mechanisms that the `ReflectionHost` is capable of statically checking. - */ - isStaticallyExported(decl: ts.Node): boolean; -} diff --git a/src/ngtsc/reflection/src/type_to_value.ts b/src/ngtsc/reflection/src/type_to_value.ts deleted file mode 100644 index 305a742be4..0000000000 --- a/src/ngtsc/reflection/src/type_to_value.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {TypeValueReference, TypeValueReferenceKind, UnavailableTypeValueReference, ValueUnavailableKind} from './host'; - -/** - * Potentially convert a `ts.TypeNode` to a `TypeValueReference`, which indicates how to use the - * type given in the `ts.TypeNode` in a value position. - * - * This can return `null` if the `typeNode` is `null`, if it does not refer to a symbol with a value - * declaration, or if it is not possible to statically understand. - */ -export function typeToValue( - typeNode: ts.TypeNode|null, checker: ts.TypeChecker): TypeValueReference { - // It's not possible to get a value expression if the parameter doesn't even have a type. - if (typeNode === null) { - return missingType(); - } - - if (!ts.isTypeReferenceNode(typeNode)) { - return unsupportedType(typeNode); - } - - const symbols = resolveTypeSymbols(typeNode, checker); - if (symbols === null) { - return unknownReference(typeNode); - } - - const {local, decl} = symbols; - // It's only valid to convert a type reference to a value reference if the type actually - // has a value declaration associated with it. Note that const enums are an exception, - // because while they do have a value declaration, they don't exist at runtime. - if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) { - let typeOnlyDecl: ts.Declaration|null = null; - if (decl.declarations !== undefined && decl.declarations.length > 0) { - typeOnlyDecl = decl.declarations[0]; - } - return noValueDeclaration(typeNode, typeOnlyDecl); - } - - // The type points to a valid value declaration. Rewrite the TypeReference into an - // Expression which references the value pointed to by the TypeReference, if possible. - - // Look at the local `ts.Symbol`'s declarations and see if it comes from an import - // statement. If so, extract the module specifier and the name of the imported type. - const firstDecl = local.declarations && local.declarations[0]; - if (firstDecl !== undefined) { - if (ts.isImportClause(firstDecl) && firstDecl.name !== undefined) { - // This is a default import. - // import Foo from 'foo'; - - if (firstDecl.isTypeOnly) { - // Type-only imports cannot be represented as value. - return typeOnlyImport(typeNode, firstDecl); - } - - return { - kind: TypeValueReferenceKind.LOCAL, - expression: firstDecl.name, - defaultImportStatement: firstDecl.parent, - }; - } else if (ts.isImportSpecifier(firstDecl)) { - // The symbol was imported by name - // import {Foo} from 'foo'; - // or - // import {Foo as Bar} from 'foo'; - - if (firstDecl.isTypeOnly) { - // The import specifier can't be type-only (e.g. `import {type Foo} from '...')`. - return typeOnlyImport(typeNode, firstDecl); - } - - if (firstDecl.parent.parent.isTypeOnly) { - // The import specifier can't be inside a type-only import clause - // (e.g. `import type {Foo} from '...')`. - return typeOnlyImport(typeNode, firstDecl.parent.parent); - } - - // Determine the name to import (`Foo`) from the import specifier, as the symbol names of - // the imported type could refer to a local alias (like `Bar` in the example above). - const importedName = (firstDecl.propertyName || firstDecl.name).text; - - // The first symbol name refers to the local name, which is replaced by `importedName` above. - // Any remaining symbol names make up the complete path to the value. - const [_localName, ...nestedPath] = symbols.symbolNames; - - const moduleName = extractModuleName(firstDecl.parent.parent.parent); - return { - kind: TypeValueReferenceKind.IMPORTED, - valueDeclaration: decl.valueDeclaration, - moduleName, - importedName, - nestedPath - }; - } else if (ts.isNamespaceImport(firstDecl)) { - // The import is a namespace import - // import * as Foo from 'foo'; - - if (firstDecl.parent.isTypeOnly) { - // Type-only imports cannot be represented as value. - return typeOnlyImport(typeNode, firstDecl.parent); - } - - if (symbols.symbolNames.length === 1) { - // The type refers to the namespace itself, which cannot be represented as a value. - return namespaceImport(typeNode, firstDecl.parent); - } - - // The first symbol name refers to the local name of the namespace, which is is discarded - // as a new namespace import will be generated. This is followed by the symbol name that needs - // to be imported and any remaining names that constitute the complete path to the value. - const [_ns, importedName, ...nestedPath] = symbols.symbolNames; - - const moduleName = extractModuleName(firstDecl.parent.parent); - return { - kind: TypeValueReferenceKind.IMPORTED, - valueDeclaration: decl.valueDeclaration, - moduleName, - importedName, - nestedPath - }; - } - } - - // If the type is not imported, the type reference can be converted into an expression as is. - const expression = typeNodeToValueExpr(typeNode); - if (expression !== null) { - return { - kind: TypeValueReferenceKind.LOCAL, - expression, - defaultImportStatement: null, - }; - } else { - return unsupportedType(typeNode); - } -} - -function unsupportedType(typeNode: ts.TypeNode): UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.UNSUPPORTED, typeNode}, - }; -} - -function noValueDeclaration( - typeNode: ts.TypeNode, decl: ts.Declaration|null): UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.NO_VALUE_DECLARATION, typeNode, decl}, - }; -} - -function typeOnlyImport(typeNode: ts.TypeNode, node: ts.ImportClause|ts.ImportSpecifier): - UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.TYPE_ONLY_IMPORT, typeNode, node}, - }; -} - -function unknownReference(typeNode: ts.TypeNode): UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.UNKNOWN_REFERENCE, typeNode}, - }; -} - -function namespaceImport( - typeNode: ts.TypeNode, importClause: ts.ImportClause): UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.NAMESPACE, typeNode, importClause}, - }; -} - -function missingType(): UnavailableTypeValueReference { - return { - kind: TypeValueReferenceKind.UNAVAILABLE, - reason: {kind: ValueUnavailableKind.MISSING_TYPE}, - }; -} - -/** - * Attempt to extract a `ts.Expression` that's equivalent to a `ts.TypeNode`, as the two have - * different AST shapes but can reference the same symbols. - * - * This will return `null` if an equivalent expression cannot be constructed. - */ -export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null { - if (ts.isTypeReferenceNode(node)) { - return entityNameToValue(node.typeName); - } else { - return null; - } -} - -/** - * Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source. - * - * In the event that the `TypeReference` refers to a locally declared symbol, these will be the - * same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved - * `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifier` - * which points to the import statement by which the symbol was imported. - * - * All symbol names that make up the type reference are returned left-to-right into the - * `symbolNames` array, which is guaranteed to include at least one entry. - */ -function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker): - {local: ts.Symbol, decl: ts.Symbol, symbolNames: string[]}|null { - const typeName = typeRef.typeName; - // typeRefSymbol is the ts.Symbol of the entire type reference. - const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName); - if (typeRefSymbol === undefined) { - return null; - } - - // `local` is the `ts.Symbol` for the local `ts.Identifier` for the type. - // If the type is actually locally declared or is imported by name, for example: - // import {Foo} from './foo'; - // then it'll be the same as `typeRefSymbol`. - // - // If the type is imported via a namespace import, for example: - // import * as foo from './foo'; - // and then referenced as: - // constructor(f: foo.Foo) - // then `local` will be the `ts.Symbol` of `foo`, whereas `typeRefSymbol` will be the `ts.Symbol` - // of `foo.Foo`. This allows tracking of the import behind whatever type reference exists. - let local = typeRefSymbol; - - // Destructure a name like `foo.X.Y.Z` as follows: - // - in `leftMost`, the `ts.Identifier` of the left-most name (`foo`) in the qualified name. - // This identifier is used to resolve the `ts.Symbol` for `local`. - // - in `symbolNames`, all names involved in the qualified path, or a single symbol name if the - // type is not qualified. - let leftMost = typeName; - const symbolNames: string[] = []; - while (ts.isQualifiedName(leftMost)) { - symbolNames.unshift(leftMost.right.text); - leftMost = leftMost.left; - } - symbolNames.unshift(leftMost.text); - - if (leftMost !== typeName) { - const localTmp = checker.getSymbolAtLocation(leftMost); - if (localTmp !== undefined) { - local = localTmp; - } - } - - // De-alias the top-level type reference symbol to get the symbol of the actual declaration. - let decl = typeRefSymbol; - if (typeRefSymbol.flags & ts.SymbolFlags.Alias) { - decl = checker.getAliasedSymbol(typeRefSymbol); - } - return {local, decl, symbolNames}; -} - -function entityNameToValue(node: ts.EntityName): ts.Expression|null { - if (ts.isQualifiedName(node)) { - const left = entityNameToValue(node.left); - return left !== null ? ts.factory.createPropertyAccessExpression(left, node.right) : null; - } else if (ts.isIdentifier(node)) { - const clone = ts.setOriginalNode(ts.factory.createIdentifier(node.text), node); - (clone as any).parent = node.parent; - return clone; - } else { - return null; - } -} - -function extractModuleName(node: ts.ImportDeclaration): string { - if (!ts.isStringLiteral(node.moduleSpecifier)) { - throw new Error('not a module specifier'); - } - return node.moduleSpecifier.text; -} diff --git a/src/ngtsc/reflection/src/typescript.ts b/src/ngtsc/reflection/src/typescript.ts deleted file mode 100644 index 2f1ee8dc12..0000000000 --- a/src/ngtsc/reflection/src/typescript.ts +++ /dev/null @@ -1,706 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {getDecorators, getModifiers} from '../../ts_compatibility'; - -import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, DeclarationKind, DeclarationNode, Decorator, FunctionDefinition, Import, isDecoratorIdentifier, ReflectionHost} from './host'; -import {typeToValue} from './type_to_value'; -import {isNamedClassDeclaration} from './util'; - -/** - * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. - */ - -export class TypeScriptReflectionHost implements ReflectionHost { - constructor(protected checker: ts.TypeChecker) {} - - getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null { - const decorators = getDecorators(declaration); - - return decorators !== undefined && decorators.length ? - decorators.map(decorator => this._reflectDecorator(decorator)) - .filter((dec): dec is Decorator => dec !== null) : - null; - } - - getMembersOfClass(clazz: ClassDeclaration): ClassMember[] { - const tsClazz = castDeclarationToClassOrDie(clazz); - return tsClazz.members.map(member => this._reflectMember(member)) - .filter((member): member is ClassMember => member !== null); - } - - getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null { - const tsClazz = castDeclarationToClassOrDie(clazz); - - const isDeclaration = tsClazz.getSourceFile().isDeclarationFile; - // For non-declaration files, we want to find the constructor with a `body`. The constructors - // without a `body` are overloads whereas we want the implementation since it's the one that'll - // be executed and which can have decorators. For declaration files, we take the first one that - // we get. - const ctor = tsClazz.members.find( - (member): member is ts.ConstructorDeclaration => - ts.isConstructorDeclaration(member) && (isDeclaration || member.body !== undefined)); - if (ctor === undefined) { - return null; - } - - return ctor.parameters.map(node => { - // The name of the parameter is easy. - const name = parameterName(node.name); - - const decorators = this.getDecoratorsOfDeclaration(node); - - // It may or may not be possible to write an expression that refers to the value side of the - // type named for the parameter. - - let originalTypeNode = node.type || null; - let typeNode = originalTypeNode; - - // Check if we are dealing with a simple nullable union type e.g. `foo: Foo|null` - // and extract the type. More complex union types e.g. `foo: Foo|Bar` are not supported. - // We also don't need to support `foo: Foo|undefined` because Angular's DI injects `null` for - // optional tokes that don't have providers. - if (typeNode && ts.isUnionTypeNode(typeNode)) { - let childTypeNodes = typeNode.types.filter( - childTypeNode => - !(ts.isLiteralTypeNode(childTypeNode) && - childTypeNode.literal.kind === ts.SyntaxKind.NullKeyword)); - - if (childTypeNodes.length === 1) { - typeNode = childTypeNodes[0]; - } - } - - const typeValueReference = typeToValue(typeNode, this.checker); - - return { - name, - nameNode: node.name, - typeValueReference, - typeNode: originalTypeNode, - decorators, - }; - }); - } - - getImportOfIdentifier(id: ts.Identifier): Import|null { - const directImport = this.getDirectImportOfIdentifier(id); - if (directImport !== null) { - return directImport; - } else if (ts.isQualifiedName(id.parent) && id.parent.right === id) { - return this.getImportOfNamespacedIdentifier(id, getQualifiedNameRoot(id.parent)); - } else if (ts.isPropertyAccessExpression(id.parent) && id.parent.name === id) { - return this.getImportOfNamespacedIdentifier(id, getFarLeftIdentifier(id.parent)); - } else { - return null; - } - } - - getExportsOfModule(node: ts.Node): Map|null { - // In TypeScript code, modules are only ts.SourceFiles. Throw if the node isn't a module. - if (!ts.isSourceFile(node)) { - throw new Error(`getExportsOfModule() called on non-SourceFile in TS code`); - } - - // Reflect the module to a Symbol, and use getExportsOfModule() to get a list of exported - // Symbols. - const symbol = this.checker.getSymbolAtLocation(node); - if (symbol === undefined) { - return null; - } - - const map = new Map(); - this.checker.getExportsOfModule(symbol).forEach(exportSymbol => { - // Map each exported Symbol to a Declaration and add it to the map. - const decl = this.getDeclarationOfSymbol(exportSymbol, null); - if (decl !== null) { - map.set(exportSymbol.name, decl); - } - }); - return map; - } - - isClass(node: ts.Node): node is ClassDeclaration { - // For our purposes, classes are "named" ts.ClassDeclarations; - // (`node.name` can be undefined in unnamed default exports: `default export class { ... }`). - return isNamedClassDeclaration(node); - } - - hasBaseClass(clazz: ClassDeclaration): boolean { - return this.getBaseClassExpression(clazz) !== null; - } - - getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { - if (!(ts.isClassDeclaration(clazz) || ts.isClassExpression(clazz)) || - clazz.heritageClauses === undefined) { - return null; - } - const extendsClause = - clazz.heritageClauses.find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); - if (extendsClause === undefined) { - return null; - } - const extendsType = extendsClause.types[0]; - if (extendsType === undefined) { - return null; - } - return extendsType.expression; - } - - getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { - // Resolve the identifier to a Symbol, and return the declaration of that. - let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id); - if (symbol === undefined) { - return null; - } - return this.getDeclarationOfSymbol(symbol, id); - } - - getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { - if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) && - !ts.isFunctionExpression(node)) { - return null; - } - return { - node, - body: node.body !== undefined ? Array.from(node.body.statements) : null, - parameters: node.parameters.map(param => { - const name = parameterName(param.name); - const initializer = param.initializer || null; - return {name, node: param, initializer}; - }), - }; - } - - getGenericArityOfClass(clazz: ClassDeclaration): number|null { - if (!ts.isClassDeclaration(clazz)) { - return null; - } - return clazz.typeParameters !== undefined ? clazz.typeParameters.length : 0; - } - - getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null { - return declaration.initializer || null; - } - - getDtsDeclaration(_: ClassDeclaration): ts.Declaration|null { - return null; - } - - getInternalNameOfClass(clazz: ClassDeclaration): ts.Identifier { - return clazz.name; - } - - getAdjacentNameOfClass(clazz: ClassDeclaration): ts.Identifier { - return clazz.name; - } - - isStaticallyExported(decl: ts.Node): boolean { - // First check if there's an `export` modifier directly on the declaration. - let topLevel = decl; - if (ts.isVariableDeclaration(decl) && ts.isVariableDeclarationList(decl.parent)) { - topLevel = decl.parent.parent; - } - const modifiers = getModifiers(topLevel); - if (modifiers !== undefined && - modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) { - // The node is part of a declaration that's directly exported. - return true; - } - - // If `topLevel` is not directly exported via a modifier, then it might be indirectly exported, - // e.g.: - // - // class Foo {} - // export {Foo}; - // - // The only way to check this is to look at the module level for exports of the class. As a - // performance optimization, this check is only performed if the class is actually declared at - // the top level of the file and thus eligible for exporting in the first place. - if (topLevel.parent === undefined || !ts.isSourceFile(topLevel.parent)) { - return false; - } - - const localExports = this.getLocalExportedDeclarationsOfSourceFile(decl.getSourceFile()); - return localExports.has(decl as ts.Declaration); - } - - protected getDirectImportOfIdentifier(id: ts.Identifier): Import|null { - const symbol = this.checker.getSymbolAtLocation(id); - - if (symbol === undefined || symbol.declarations === undefined || - symbol.declarations.length !== 1) { - return null; - } - - const decl = symbol.declarations[0]; - const importDecl = getContainingImportDeclaration(decl); - - // Ignore declarations that are defined locally (not imported). - if (importDecl === null) { - return null; - } - - // The module specifier is guaranteed to be a string literal, so this should always pass. - if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { - // Not allowed to happen in TypeScript ASTs. - return null; - } - - return {from: importDecl.moduleSpecifier.text, name: getExportedName(decl, id)}; - } - - /** - * Try to get the import info for this identifier as though it is a namespaced import. - * - * For example, if the identifier is the `Directive` part of a qualified type chain like: - * - * ``` - * core.Directive - * ``` - * - * then it might be that `core` is a namespace import such as: - * - * ``` - * import * as core from 'tslib'; - * ``` - * - * @param id the TypeScript identifier to find the import info for. - * @returns The import info if this is a namespaced import or `null`. - */ - protected getImportOfNamespacedIdentifier( - id: ts.Identifier, namespaceIdentifier: ts.Identifier|null): Import|null { - if (namespaceIdentifier === null) { - return null; - } - const namespaceSymbol = this.checker.getSymbolAtLocation(namespaceIdentifier); - if (!namespaceSymbol || namespaceSymbol.declarations === undefined) { - return null; - } - const declaration = - namespaceSymbol.declarations.length === 1 ? namespaceSymbol.declarations[0] : null; - if (!declaration) { - return null; - } - const namespaceDeclaration = ts.isNamespaceImport(declaration) ? declaration : null; - if (!namespaceDeclaration) { - return null; - } - - const importDeclaration = namespaceDeclaration.parent.parent; - if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) { - // Should not happen as this would be invalid TypesScript - return null; - } - - return { - from: importDeclaration.moduleSpecifier.text, - name: id.text, - }; - } - - /** - * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. - */ - protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration - |null { - // If the symbol points to a ShorthandPropertyAssignment, resolve it. - let valueDeclaration: ts.Declaration|undefined = undefined; - if (symbol.valueDeclaration !== undefined) { - valueDeclaration = symbol.valueDeclaration; - } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { - valueDeclaration = symbol.declarations[0]; - } - if (valueDeclaration !== undefined && ts.isShorthandPropertyAssignment(valueDeclaration)) { - const shorthandSymbol = this.checker.getShorthandAssignmentValueSymbol(valueDeclaration); - if (shorthandSymbol === undefined) { - return null; - } - return this.getDeclarationOfSymbol(shorthandSymbol, originalId); - } else if (valueDeclaration !== undefined && ts.isExportSpecifier(valueDeclaration)) { - const targetSymbol = this.checker.getExportSpecifierLocalTargetSymbol(valueDeclaration); - if (targetSymbol === undefined) { - return null; - } - return this.getDeclarationOfSymbol(targetSymbol, originalId); - } - - const importInfo = originalId && this.getImportOfIdentifier(originalId); - const viaModule = - importInfo !== null && importInfo.from !== null && !importInfo.from.startsWith('.') ? - importInfo.from : - null; - - // Now, resolve the Symbol to its declaration by following any and all aliases. - while (symbol.flags & ts.SymbolFlags.Alias) { - symbol = this.checker.getAliasedSymbol(symbol); - } - - // Look at the resolved Symbol's declarations and pick one of them to return. Value declarations - // are given precedence over type declarations. - if (symbol.valueDeclaration !== undefined) { - return { - node: symbol.valueDeclaration, - known: null, - viaModule, - identity: null, - kind: DeclarationKind.Concrete, - }; - } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { - return { - node: symbol.declarations[0], - known: null, - viaModule, - identity: null, - kind: DeclarationKind.Concrete, - }; - } else { - return null; - } - } - - private _reflectDecorator(node: ts.Decorator): Decorator|null { - // Attempt to resolve the decorator expression into a reference to a concrete Identifier. The - // expression may contain a call to a function which returns the decorator function, in which - // case we want to return the arguments. - let decoratorExpr: ts.Expression = node.expression; - let args: ts.Expression[]|null = null; - - // Check for call expressions. - if (ts.isCallExpression(decoratorExpr)) { - args = Array.from(decoratorExpr.arguments); - decoratorExpr = decoratorExpr.expression; - } - - // The final resolved decorator should be a `ts.Identifier` - if it's not, then something is - // wrong and the decorator can't be resolved statically. - if (!isDecoratorIdentifier(decoratorExpr)) { - return null; - } - - const decoratorIdentifier = ts.isIdentifier(decoratorExpr) ? decoratorExpr : decoratorExpr.name; - const importDecl = this.getImportOfIdentifier(decoratorIdentifier); - - return { - name: decoratorIdentifier.text, - identifier: decoratorExpr, - import: importDecl, - node, - args, - }; - } - - private _reflectMember(node: ts.ClassElement): ClassMember|null { - let kind: ClassMemberKind|null = null; - let value: ts.Expression|null = null; - let name: string|null = null; - let nameNode: ts.Identifier|ts.StringLiteral|null = null; - - if (ts.isPropertyDeclaration(node)) { - kind = ClassMemberKind.Property; - value = node.initializer || null; - } else if (ts.isGetAccessorDeclaration(node)) { - kind = ClassMemberKind.Getter; - } else if (ts.isSetAccessorDeclaration(node)) { - kind = ClassMemberKind.Setter; - } else if (ts.isMethodDeclaration(node)) { - kind = ClassMemberKind.Method; - } else if (ts.isConstructorDeclaration(node)) { - kind = ClassMemberKind.Constructor; - } else { - return null; - } - - if (ts.isConstructorDeclaration(node)) { - name = 'constructor'; - } else if (ts.isIdentifier(node.name)) { - name = node.name.text; - nameNode = node.name; - } else if (ts.isStringLiteral(node.name)) { - name = node.name.text; - nameNode = node.name; - } else { - return null; - } - - const decorators = this.getDecoratorsOfDeclaration(node); - const modifiers = getModifiers(node); - const isStatic = - modifiers !== undefined && modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); - - return { - node, - implementation: node, - kind, - type: node.type || null, - name, - nameNode, - decorators, - value, - isStatic, - }; - } - - /** - * Get the set of declarations declared in `file` which are exported. - */ - private getLocalExportedDeclarationsOfSourceFile(file: ts.SourceFile): Set { - const cacheSf: SourceFileWithCachedExports = file as SourceFileWithCachedExports; - if (cacheSf[LocalExportedDeclarations] !== undefined) { - // TS does not currently narrow symbol-keyed fields, hence the non-null assert is needed. - return cacheSf[LocalExportedDeclarations]!; - } - - const exportSet = new Set(); - cacheSf[LocalExportedDeclarations] = exportSet; - - const sfSymbol = this.checker.getSymbolAtLocation(cacheSf); - - if (sfSymbol === undefined || sfSymbol.exports === undefined) { - return exportSet; - } - - // Scan the exported symbol of the `ts.SourceFile` for the original `symbol` of the class - // declaration. - // - // Note: when checking multiple classes declared in the same file, this repeats some operations. - // In theory, this could be expensive if run in the context of a massive input file (like a - // large FESM in ngcc). If performance does become an issue here, it should be possible to - // create a `Set<>` - - // Unfortunately, `ts.Iterator` doesn't implement the iterator protocol, so iteration here is - // done manually. - const iter = sfSymbol.exports.values(); - let item = iter.next(); - while (item.done !== true) { - let exportedSymbol = item.value; - - // If this exported symbol comes from an `export {Foo}` statement, then the symbol is actually - // for the export declaration, not the original declaration. Such a symbol will be an alias, - // so unwrap aliasing if necessary. - if (exportedSymbol.flags & ts.SymbolFlags.Alias) { - exportedSymbol = this.checker.getAliasedSymbol(exportedSymbol); - } - - if (exportedSymbol.valueDeclaration !== undefined && - exportedSymbol.valueDeclaration.getSourceFile() === file) { - exportSet.add(exportedSymbol.valueDeclaration); - } - item = iter.next(); - } - - return exportSet; - } -} - -export function reflectNameOfDeclaration(decl: ts.Declaration): string|null { - const id = reflectIdentifierOfDeclaration(decl); - return id && id.text || null; -} - -export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier|null { - if (ts.isClassDeclaration(decl) || ts.isFunctionDeclaration(decl)) { - return decl.name || null; - } else if (ts.isVariableDeclaration(decl)) { - if (ts.isIdentifier(decl.name)) { - return decl.name; - } - } - return null; -} - -export function reflectTypeEntityToDeclaration( - type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string|null} { - let realSymbol = checker.getSymbolAtLocation(type); - if (realSymbol === undefined) { - throw new Error(`Cannot resolve type entity ${type.getText()} to symbol`); - } - while (realSymbol.flags & ts.SymbolFlags.Alias) { - realSymbol = checker.getAliasedSymbol(realSymbol); - } - - let node: ts.Declaration|null = null; - if (realSymbol.valueDeclaration !== undefined) { - node = realSymbol.valueDeclaration; - } else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) { - node = realSymbol.declarations[0]; - } else { - throw new Error(`Cannot resolve type entity symbol to declaration`); - } - - if (ts.isQualifiedName(type)) { - if (!ts.isIdentifier(type.left)) { - throw new Error(`Cannot handle qualified name with non-identifier lhs`); - } - const symbol = checker.getSymbolAtLocation(type.left); - if (symbol === undefined || symbol.declarations === undefined || - symbol.declarations.length !== 1) { - throw new Error(`Cannot resolve qualified type entity lhs to symbol`); - } - const decl = symbol.declarations[0]; - if (ts.isNamespaceImport(decl)) { - const clause = decl.parent!; - const importDecl = clause.parent!; - if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { - throw new Error(`Module specifier is not a string`); - } - return {node, from: importDecl.moduleSpecifier.text}; - } else if (ts.isModuleDeclaration(decl)) { - return {node, from: null}; - } else { - throw new Error(`Unknown import type?`); - } - } else { - return {node, from: null}; - } -} - -export function filterToMembersWithDecorator(members: ClassMember[], name: string, module?: string): - {member: ClassMember, decorators: Decorator[]}[] { - return members.filter(member => !member.isStatic) - .map(member => { - if (member.decorators === null) { - return null; - } - - const decorators = member.decorators.filter(dec => { - if (dec.import !== null) { - return dec.import.name === name && (module === undefined || dec.import.from === module); - } else { - return dec.name === name && module === undefined; - } - }); - - if (decorators.length === 0) { - return null; - } - - return {member, decorators}; - }) - .filter((value): value is {member: ClassMember, decorators: Decorator[]} => value !== null); -} - -export function findMember( - members: ClassMember[], name: string, isStatic: boolean = false): ClassMember|null { - return members.find(member => member.isStatic === isStatic && member.name === name) || null; -} - -export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map { - const map = new Map(); - node.properties.forEach(prop => { - if (ts.isPropertyAssignment(prop)) { - const name = propertyNameToString(prop.name); - if (name === null) { - return; - } - map.set(name, prop.initializer); - } else if (ts.isShorthandPropertyAssignment(prop)) { - map.set(prop.name.text, prop.name); - } else { - return; - } - }); - return map; -} - -function castDeclarationToClassOrDie(declaration: ClassDeclaration): - ClassDeclaration { - if (!ts.isClassDeclaration(declaration)) { - throw new Error( - `Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`); - } - return declaration; -} - -function parameterName(name: ts.BindingName): string|null { - if (ts.isIdentifier(name)) { - return name.text; - } else { - return null; - } -} - -function propertyNameToString(node: ts.PropertyName): string|null { - if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { - return node.text; - } else { - return null; - } -} - -/** - * Compute the left most identifier in a qualified type chain. E.g. the `a` of `a.b.c.SomeType`. - * @param qualifiedName The starting property access expression from which we want to compute - * the left most identifier. - * @returns the left most identifier in the chain or `null` if it is not an identifier. - */ -function getQualifiedNameRoot(qualifiedName: ts.QualifiedName): ts.Identifier|null { - while (ts.isQualifiedName(qualifiedName.left)) { - qualifiedName = qualifiedName.left; - } - return ts.isIdentifier(qualifiedName.left) ? qualifiedName.left : null; -} - -/** - * Compute the left most identifier in a property access chain. E.g. the `a` of `a.b.c.d`. - * @param propertyAccess The starting property access expression from which we want to compute - * the left most identifier. - * @returns the left most identifier in the chain or `null` if it is not an identifier. - */ -function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier|null { - while (ts.isPropertyAccessExpression(propertyAccess.expression)) { - propertyAccess = propertyAccess.expression; - } - return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null; -} - -/** - * Return the ImportDeclaration for the given `node` if it is either an `ImportSpecifier` or a - * `NamespaceImport`. If not return `null`. - */ -function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclaration|null { - return ts.isImportSpecifier(node) ? node.parent!.parent!.parent! : - ts.isNamespaceImport(node) ? node.parent.parent : - null; -} - -/** - * Compute the name by which the `decl` was exported, not imported. - * If no such declaration can be found (e.g. it is a namespace import) - * then fallback to the `originalId`. - */ -function getExportedName(decl: ts.Declaration, originalId: ts.Identifier): string { - return ts.isImportSpecifier(decl) ? - (decl.propertyName !== undefined ? decl.propertyName : decl.name).text : - originalId.text; -} - -const LocalExportedDeclarations = Symbol('LocalExportedDeclarations'); - -/** - * A `ts.SourceFile` expando which includes a cached `Set` of local `ts.Declaration`s that are - * exported either directly (`export class ...`) or indirectly (via `export {...}`). - * - * This cache does not cause memory leaks as: - * - * 1. The only references cached here are local to the `ts.SourceFile`, and thus also available in - * `this.statements`. - * - * 2. The only way this `Set` could change is if the source file itself was changed, which would - * invalidate the entire `ts.SourceFile` object in favor of a new version. Thus, changing the - * source file also invalidates this cache. - */ -interface SourceFileWithCachedExports extends ts.SourceFile { - /** - * Cached `Set` of `ts.Declaration`s which are locally declared in this file and are exported - * either directly or indirectly. - */ - [LocalExportedDeclarations]?: Set; -} diff --git a/src/ngtsc/reflection/src/util.ts b/src/ngtsc/reflection/src/util.ts deleted file mode 100644 index c533e6c0a1..0000000000 --- a/src/ngtsc/reflection/src/util.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {ClassDeclaration} from './host'; - -export function isNamedClassDeclaration(node: ts.Node): - node is ClassDeclaration { - return ts.isClassDeclaration(node) && isIdentifier(node.name); -} - -export function isNamedFunctionDeclaration(node: ts.Node): - node is ClassDeclaration { - return ts.isFunctionDeclaration(node) && isIdentifier(node.name); -} - -export function isNamedVariableDeclaration(node: ts.Node): - node is ClassDeclaration { - return ts.isVariableDeclaration(node) && isIdentifier(node.name); -} - -function isIdentifier(node: ts.Node|undefined): node is ts.Identifier { - return node !== undefined && ts.isIdentifier(node); -} diff --git a/src/ngtsc/ts_compatibility/index.ts b/src/ngtsc/ts_compatibility/index.ts deleted file mode 100644 index c45d30725f..0000000000 --- a/src/ngtsc/ts_compatibility/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 './src/ts_cross_version_utils'; diff --git a/src/ngtsc/ts_compatibility/src/ts_cross_version_utils.ts b/src/ngtsc/ts_compatibility/src/ts_cross_version_utils.ts deleted file mode 100644 index f9f7c7dd4e..0000000000 --- a/src/ngtsc/ts_compatibility/src/ts_cross_version_utils.ts +++ /dev/null @@ -1,352 +0,0 @@ -/*! - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** Whether the current TypeScript version is after 4.8. */ -const IS_AFTER_TS_48 = isAfterVersion(4, 8); - -/** Equivalent of `ts.ModifierLike` which is only present in TS 4.8+. */ -export type ModifierLike = ts.Modifier|ts.Decorator; - -/** Type of `ts.factory.updateParameterDeclaration` in TS 4.8+. */ -type Ts48UpdateParameterDeclarationFn = - (node: ts.ParameterDeclaration, modifiers: readonly ModifierLike[]|undefined, - dotDotDotToken: ts.DotDotDotToken|undefined, name: string|ts.BindingName, - questionToken: ts.QuestionToken|undefined, type: ts.TypeNode|undefined, - initializer: ts.Expression|undefined) => ts.ParameterDeclaration; - -/** - * Updates a `ts.ParameterDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateParameterDeclaration: Ts48UpdateParameterDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateParameterDeclaration as any) : - (node, modifiers, dotDotDotToken, name, questionToken, type, initializer) => ( - ts.factory.updateParameterDeclaration as any)( - node, ...splitModifiers(modifiers), dotDotDotToken, name, questionToken, type, initializer); - -/** Type of `ts.factory.updateImportDeclaration` in TS 4.8+. */ -type Ts48UpdateImportDeclarationFn = - (node: ts.ImportDeclaration, modifiers: readonly ts.Modifier[]|undefined, - importClause: ts.ImportClause|undefined, moduleSpecifier: ts.Expression, - assertClause: ts.AssertClause|undefined) => ts.ImportDeclaration; - -/** - * Updates a `ts.ImportDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateImportDeclaration: Ts48UpdateImportDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateImportDeclaration as any) : - (node, modifiers, importClause, moduleSpecifier, assertClause) => - (ts.factory.updateImportDeclaration as any)( - node, undefined, modifiers, importClause, moduleSpecifier, assertClause); - -/** Type of `ts.factory.updateClassDeclaration` in TS 4.8+. */ -type Ts48UpdateClassDeclarationFn = - (node: ts.ClassDeclaration, modifiers: readonly ModifierLike[]|undefined, - name: ts.Identifier|undefined, - typeParameters: readonly ts.TypeParameterDeclaration[]|undefined, - heritageClauses: readonly ts.HeritageClause[]|undefined, - members: readonly ts.ClassElement[]) => ts.ClassDeclaration; - -/** - * Updates a `ts.ClassDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateClassDeclaration: Ts48UpdateClassDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateClassDeclaration as any) : - (node, combinedModifiers, name, typeParameters, heritageClauses, members) => ( - ts.factory.updateClassDeclaration as any)( - node, ...splitModifiers(combinedModifiers), name, typeParameters, heritageClauses, members); - -/** Type of `ts.factory.createClassDeclaration` in TS 4.8+. */ -type Ts48CreateClassDeclarationFn = - (modifiers: readonly ModifierLike[]|undefined, name: ts.Identifier|undefined, - typeParameters: readonly ts.TypeParameterDeclaration[]|undefined, - heritageClauses: readonly ts.HeritageClause[]|undefined, - members: readonly ts.ClassElement[]) => ts.ClassDeclaration; - -/** - * Creates a `ts.ClassDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const createClassDeclaration: Ts48CreateClassDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.createClassDeclaration as any) : - (combinedModifiers, name, typeParameters, heritageClauses, members) => - (ts.factory.createClassDeclaration as any)( - ...splitModifiers(combinedModifiers), name, typeParameters, heritageClauses, members); - -/** Type of `ts.factory.updateMethodDeclaration` in TS 4.8+. */ -type Ts48UpdateMethodDeclarationFn = - (node: ts.MethodDeclaration, modifiers: readonly ModifierLike[]|undefined, - asteriskToken: ts.AsteriskToken|undefined, name: ts.PropertyName, - questionToken: ts.QuestionToken|undefined, - typeParameters: readonly ts.TypeParameterDeclaration[]|undefined, - parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode|undefined, - body: ts.Block|undefined) => ts.MethodDeclaration; - -/** - * Updates a `ts.MethodDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateMethodDeclaration: Ts48UpdateMethodDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateMethodDeclaration as any) : - (node, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body) => - (ts.factory.updateMethodDeclaration as any)( - node, ...splitModifiers(modifiers), asteriskToken, name, questionToken, typeParameters, - parameters, type, body); - -/** Type of `ts.factory.createMethodDeclaration` in TS 4.8+. */ -type Ts48CreateMethodDeclarationFn = - (modifiers: readonly ModifierLike[]|undefined, asteriskToken: ts.AsteriskToken|undefined, - name: ts.PropertyName, questionToken: ts.QuestionToken|undefined, - typeParameters: readonly ts.TypeParameterDeclaration[]|undefined, - parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode|undefined, - body: ts.Block|undefined) => ts.MethodDeclaration; - -/** - * Creates a `ts.MethodDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const createMethodDeclaration: Ts48CreateMethodDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.createMethodDeclaration as any) : - (modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body) => - (ts.factory.createMethodDeclaration as any)( - ...splitModifiers(modifiers), asteriskToken, name, questionToken, typeParameters, - parameters, type, body); - -/** Type of `ts.factory.updatePropertyDeclaration` in TS 4.8+. */ -type Ts48UpdatePropertyDeclarationFn = - (node: ts.PropertyDeclaration, modifiers: readonly ModifierLike[]|undefined, - name: string|ts.PropertyName, - questionOrExclamationToken: ts.QuestionToken|ts.ExclamationToken|undefined, - type: ts.TypeNode|undefined, initializer: ts.Expression|undefined) => ts.PropertyDeclaration; - -/** - * Updates a `ts.PropertyDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updatePropertyDeclaration: Ts48UpdatePropertyDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updatePropertyDeclaration as any) : - (node, modifiers, name, questionOrExclamationToken, type, initializer) => ( - ts.factory.updatePropertyDeclaration as any)( - node, ...splitModifiers(modifiers), name, questionOrExclamationToken, type, initializer); - - -/** Type of `ts.factory.createPropertyDeclaration` in TS 4.8+. */ -type Ts48CreatePropertyDeclarationFn = - (modifiers: readonly ModifierLike[]|undefined, name: string|ts.PropertyName, - questionOrExclamationToken: ts.QuestionToken|ts.ExclamationToken|undefined, - type: ts.TypeNode|undefined, initializer: ts.Expression|undefined) => ts.PropertyDeclaration; - -/** - * Creates a `ts.PropertyDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const createPropertyDeclaration: Ts48CreatePropertyDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.createPropertyDeclaration as any) : - (modifiers, name, questionOrExclamationToken, type, initializer) => - (ts.factory.createPropertyDeclaration as any)( - ...splitModifiers(modifiers), name, questionOrExclamationToken, type, initializer); - -/** Type of `ts.factory.updateGetAccessorDeclaration` in TS 4.8+. */ -type Ts48UpdateGetAccessorDeclarationFn = - (node: ts.GetAccessorDeclaration, modifiers: readonly ModifierLike[]|undefined, - name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], - type: ts.TypeNode|undefined, body: ts.Block|undefined) => ts.GetAccessorDeclaration; - -/** - * Updates a `ts.GetAccessorDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateGetAccessorDeclaration: Ts48UpdateGetAccessorDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateGetAccessorDeclaration as any) : - (node, modifiers, name, parameters, type, body) => - (ts.factory.updateGetAccessorDeclaration as any)( - node, ...splitModifiers(modifiers), name, parameters, type, body); - -/** Type of `ts.factory.createGetAccessorDeclaration` in TS 4.8+. */ -type Ts48CreateGetAccessorDeclarationFn = - (modifiers: readonly ModifierLike[]|undefined, name: ts.PropertyName, - parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode|undefined, - body: ts.Block|undefined) => ts.GetAccessorDeclaration; - -/** - * Creates a `ts.GetAccessorDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const createGetAccessorDeclaration: Ts48CreateGetAccessorDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.createGetAccessorDeclaration as any) : - (modifiers, name, parameters, type, body) => (ts.factory.createGetAccessorDeclaration as any)( - ...splitModifiers(modifiers), name, parameters, type, body); - -/** Type of `ts.factory.updateSetAccessorDeclaration` in TS 4.8+. */ -type Ts48UpdateSetAccessorDeclarationFn = - (node: ts.SetAccessorDeclaration, modifiers: readonly ModifierLike[]|undefined, - name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[], - body: ts.Block|undefined) => ts.SetAccessorDeclaration; - -/** - * Updates a `ts.GetAccessorDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateSetAccessorDeclaration: Ts48UpdateSetAccessorDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateSetAccessorDeclaration as any) : - (node, modifiers, name, parameters, body) => (ts.factory.updateSetAccessorDeclaration as any)( - node, ...splitModifiers(modifiers), name, parameters, body); - -/** Type of `ts.factory.createSetAccessorDeclaration` in TS 4.8+. */ -type Ts48CreateSetAccessorDeclarationFn = - (modifiers: readonly ModifierLike[]|undefined, name: ts.PropertyName, - parameters: readonly ts.ParameterDeclaration[], body: ts.Block|undefined) => - ts.SetAccessorDeclaration; - -/** - * Creates a `ts.GetAccessorDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const createSetAccessorDeclaration: Ts48CreateSetAccessorDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.createSetAccessorDeclaration as any) : - (modifiers, name, parameters, body) => (ts.factory.createSetAccessorDeclaration as any)( - ...splitModifiers(modifiers), name, parameters, body); - -/** Type of `ts.factory.updateConstructorDeclaration` in TS 4.8+. */ -type Ts48UpdateConstructorDeclarationFn = - (node: ts.ConstructorDeclaration, modifiers: readonly ts.Modifier[]|undefined, - parameters: readonly ts.ParameterDeclaration[], body: ts.Block|undefined) => - ts.ConstructorDeclaration; - -/** - * Updates a `ts.ConstructorDeclaration` declaration. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const updateConstructorDeclaration: Ts48UpdateConstructorDeclarationFn = IS_AFTER_TS_48 ? - (ts.factory.updateConstructorDeclaration as any) : - (node, modifiers, parameters, body) => (ts.factory.updateConstructorDeclaration as any)( - node, undefined, modifiers, parameters, body); - -/** - * Gets the decorators that have been applied to a node. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const getDecorators: (node: ts.Node) => readonly ts.Decorator[] | undefined = - IS_AFTER_TS_48 ? (ts.getDecorators as (node: ts.Node) => readonly ts.Decorator[] | undefined) - : node => (node as any).decorators; - -/** - * Gets the modifiers that have been set on a node. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export const getModifiers: (node: ts.Node) => readonly ts.Modifier[] | undefined = - IS_AFTER_TS_48 ? (ts.getModifiers as (node: ts.Node) => readonly ts.Modifier[] | undefined) - : node => (node as any).modifiers; - -/** - * Combines an optional array of decorators with an optional array of modifiers into a single - * `ts.ModifierLike` array. Used in version of TypeScript after 4.8 where the `decorators` and - * `modifiers` arrays have been combined. - * - * TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8. - * We should remove it once we have dropped support for the older versions. - */ -export function combineModifiers( - decorators: readonly ts.Decorator[]|undefined, - modifiers: readonly ModifierLike[]|undefined): readonly ModifierLike[]|undefined { - const hasDecorators = decorators?.length; - const hasModifiers = modifiers?.length; - - // This function can be written more compactly, but it is somewhat performance-sensitive - // so we have some additional logic only to create new arrays when necessary. - if (hasDecorators && hasModifiers) { - return [...decorators, ...modifiers]; - } - - if (hasDecorators && !hasModifiers) { - return decorators; - } - - if (hasModifiers && !hasDecorators) { - return modifiers; - } - - return undefined; -} - -/** - * Splits a `ModifierLike` into two arrays: decorators and modifiers. Used for backwards - * compatibility with TS 4.7 and below where most factory functions require separate `decorators` - * and `modifiers` arrays. - */ -function splitModifiers(allModifiers: readonly ModifierLike[]| - undefined): [ts.Decorator[]|undefined, ts.Modifier[]|undefined] { - if (!allModifiers) { - return [undefined, undefined]; - } - - const decorators: ts.Decorator[] = []; - const modifiers: ts.Modifier[] = []; - - for (const current of allModifiers) { - if (ts.isDecorator(current)) { - decorators.push(current); - } else { - modifiers.push(current); - } - } - - return [decorators.length ? decorators : undefined, modifiers.length ? modifiers : undefined]; -} - -/** Checks if the current version of TypeScript is after the specified major/minor versions. */ -function isAfterVersion(targetMajor: number, targetMinor: number): boolean { - const [major, minor] = ts.versionMajorMinor.split('.').map(part => parseInt(part)); - - if (major < targetMajor) { - return false; - } - - return major === targetMajor ? minor >= targetMinor : true; -} - -export function toMutableArray(immutableArray: readonly T[] | undefined): T[] | undefined { - if (immutableArray) { - return [...immutableArray]; - } - return undefined; -} diff --git a/src/transformers/downlevel-ctor.ts b/src/transformers/downlevel-ctor.ts deleted file mode 100644 index 04810b85c9..0000000000 --- a/src/transformers/downlevel-ctor.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type ts from 'typescript'; - -import { TypeScriptReflectionHost } from '../ngtsc/reflection'; - -import { getDownlevelDecoratorsTransform } from './downlevel_decorators_transform'; - -export const constructorParametersDownlevelTransform = (program: ts.Program): ts.TransformerFactory => { - const typeChecker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(typeChecker); - - return getDownlevelDecoratorsTransform(typeChecker, reflectionHost, [], false, false, true); -}; diff --git a/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts b/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts deleted file mode 100644 index 21b7a984d6..0000000000 --- a/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts +++ /dev/null @@ -1,627 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -import {Decorator, ReflectionHost} from '../../ngtsc/reflection'; -import { - combineModifiers, - createGetAccessorDeclaration, - createMethodDeclaration, - createPropertyDeclaration, - createSetAccessorDeclaration, - getDecorators, - getModifiers, - ModifierLike, - updateClassDeclaration, - updateConstructorDeclaration, - updateParameterDeclaration, -} from '../../ngtsc/ts_compatibility'; - -import {isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './patch_alias_reference_resolution'; - -/** - * Whether a given decorator should be treated as an Angular decorator. - * Either it's used in @angular/core, or it's imported from there. - */ -function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean { - return isCore || (decorator.import !== null && decorator.import.from === '@angular/core'); -} - -/* - ##################################################################### - Code below has been extracted from the tsickle decorator downlevel transformer - and a few local modifications have been applied: - - 1. Tsickle by default processed all decorators that had the `@Annotation` JSDoc. - We modified the transform to only be concerned with known Angular decorators. - 2. Tsickle by default added `@nocollapse` to all generated `ctorParameters` properties. - We only do this when `annotateForClosureCompiler` is enabled. - 3. Tsickle does not handle union types for dependency injection. i.e. if a injected type - is denoted with `@Optional`, the actual type could be set to `T | null`. - See: https://github.com/angular/angular-cli/commit/826803d0736b807867caff9f8903e508970ad5e4. - 4. Tsickle relied on `emitDecoratorMetadata` to be set to `true`. This is due to a limitation - in TypeScript transformers that never has been fixed. We were able to work around this - limitation so that `emitDecoratorMetadata` doesn't need to be specified. - See: `patchAliasReferenceResolution` for more details. - - Here is a link to the tsickle revision on which this transformer is based: - https://github.com/angular/tsickle/blob/fae06becb1570f491806060d83f29f2d50c43cdd/src/decorator_downlevel_transformer.ts - ##################################################################### -*/ - -const DECORATOR_INVOCATION_JSDOC_TYPE = '!Array<{type: !Function, args: (undefined|!Array)}>'; - -/** - * Extracts the type of the decorator (the function or expression invoked), as well as all the - * arguments passed to the decorator. Returns an AST with the form: - * - * // For @decorator(arg1, arg2) - * { type: decorator, args: [arg1, arg2] } - */ -function extractMetadataFromSingleDecorator( - decorator: ts.Decorator, diagnostics: ts.Diagnostic[]): ts.ObjectLiteralExpression { - const metadataProperties: ts.ObjectLiteralElementLike[] = []; - const expr = decorator.expression; - switch (expr.kind) { - case ts.SyntaxKind.Identifier: - // The decorator was a plain @Foo. - metadataProperties.push(ts.factory.createPropertyAssignment('type', expr)); - break; - case ts.SyntaxKind.CallExpression: - // The decorator was a call, like @Foo(bar). - const call = expr as ts.CallExpression; - metadataProperties.push(ts.factory.createPropertyAssignment('type', call.expression)); - if (call.arguments.length) { - const args: ts.Expression[] = []; - for (const arg of call.arguments) { - args.push(arg); - } - const argsArrayLiteral = - ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(args, true)); - metadataProperties.push(ts.factory.createPropertyAssignment('args', argsArrayLiteral)); - } - break; - default: - diagnostics.push({ - file: decorator.getSourceFile(), - start: decorator.getStart(), - length: decorator.getEnd() - decorator.getStart(), - messageText: - `${ts.SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata.`, - category: ts.DiagnosticCategory.Error, - code: 0, - }); - break; - } - return ts.factory.createObjectLiteralExpression(metadataProperties); -} - -/** - * createCtorParametersClassProperty creates a static 'ctorParameters' property containing - * downleveled decorator information. - * - * The property contains an arrow function that returns an array of object literals of the shape: - * static ctorParameters = () => [{ - * type: SomeClass|undefined, // the type of the param that's decorated, if it's a value. - * decorators: [{ - * type: DecoratorFn, // the type of the decorator that's invoked. - * args: [ARGS], // the arguments passed to the decorator. - * }] - * }]; - */ -function createCtorParametersClassProperty( - diagnostics: ts.Diagnostic[], - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - ctorParameters: ParameterDecorationInfo[], - isClosureCompilerEnabled: boolean): ts.PropertyDeclaration { - const params: ts.Expression[] = []; - - for (const ctorParam of ctorParameters) { - if (!ctorParam.type && ctorParam.decorators.length === 0) { - params.push(ts.factory.createNull()); - continue; - } - - const paramType = ctorParam.type ? - typeReferenceToExpression(entityNameToExpression, ctorParam.type) : - undefined; - const members = [ts.factory.createPropertyAssignment( - 'type', paramType || ts.factory.createIdentifier('undefined'))]; - - const decorators: ts.ObjectLiteralExpression[] = []; - for (const deco of ctorParam.decorators) { - decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); - } - if (decorators.length) { - members.push(ts.factory.createPropertyAssignment( - 'decorators', ts.factory.createArrayLiteralExpression(decorators))); - } - params.push(ts.factory.createObjectLiteralExpression(members)); - } - - const initializer = ts.factory.createArrowFunction( - undefined, undefined, [], undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createArrayLiteralExpression(params, true)); - const ctorProp = createPropertyDeclaration( - [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'ctorParameters', undefined, undefined, - initializer); - if (isClosureCompilerEnabled) { - ts.setSyntheticLeadingComments(ctorProp, [ - { - kind: ts.SyntaxKind.MultiLineCommentTrivia, - text: [ - `*`, - ` * @type {function(): !Array<(null|{`, - ` * type: ?,`, - ` * decorators: (undefined|${DECORATOR_INVOCATION_JSDOC_TYPE}),`, - ` * })>}`, - ` * @nocollapse`, - ` `, - ].join('\n'), - pos: -1, - end: -1, - hasTrailingNewLine: true, - }, - ]); - } - return ctorProp; -} - -/** - * Returns an expression representing the (potentially) value part for the given node. - * - * This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a - * workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode - * not being exposed). In practice this implementation is sufficient for Angular's use of type - * metadata. - */ -function typeReferenceToExpression( - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - node: ts.TypeNode): ts.Expression|undefined { - let kind = node.kind; - if (ts.isLiteralTypeNode(node)) { - // Treat literal types like their base type (boolean, string, number). - kind = node.literal.kind; - } - switch (kind) { - case ts.SyntaxKind.FunctionType: - case ts.SyntaxKind.ConstructorType: - return ts.factory.createIdentifier('Function'); - case ts.SyntaxKind.ArrayType: - case ts.SyntaxKind.TupleType: - return ts.factory.createIdentifier('Array'); - case ts.SyntaxKind.TypePredicate: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.BooleanKeyword: - return ts.factory.createIdentifier('Boolean'); - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.StringKeyword: - return ts.factory.createIdentifier('String'); - case ts.SyntaxKind.ObjectKeyword: - return ts.factory.createIdentifier('Object'); - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.NumericLiteral: - return ts.factory.createIdentifier('Number'); - case ts.SyntaxKind.TypeReference: - const typeRef = node as ts.TypeReferenceNode; - // Ignore any generic types, just return the base type. - return entityNameToExpression(typeRef.typeName); - case ts.SyntaxKind.UnionType: - const childTypeNodes = - (node as ts.UnionTypeNode) - .types.filter( - t => !(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword)); - return childTypeNodes.length === 1 ? - typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) : - undefined; - default: - return undefined; - } -} - -/** - * Checks whether a given symbol refers to a value that exists at runtime (as distinct from a type). - * - * Expands aliases, which is important for the case where - * import * as x from 'some-module'; - * and x is now a value (the module object). - */ -function symbolIsRuntimeValue(typeChecker: ts.TypeChecker, symbol: ts.Symbol): boolean { - if (symbol.flags & ts.SymbolFlags.Alias) { - symbol = typeChecker.getAliasedSymbol(symbol); - } - - // Note that const enums are a special case, because - // while they have a value, they don't exist at runtime. - return (symbol.flags & ts.SymbolFlags.Value & ts.SymbolFlags.ConstEnumExcludes) !== 0; -} - -/** ParameterDecorationInfo describes the information for a single constructor parameter. */ -interface ParameterDecorationInfo { - /** - * The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an - * interface). - */ - type: ts.TypeNode|null; - /** The list of decorators found on the parameter, null if none. */ - decorators: ts.Decorator[]; -} - -/** - * Gets a transformer for downleveling Angular decorators. - * @param typeChecker Reference to the program's type checker. - * @param host Reflection host that is used for determining decorators. - * @param diagnostics List which will be populated with diagnostics if any. - * @param isCore Whether the current TypeScript program is for the `@angular/core` package. - * @param isClosureCompilerEnabled Whether closure annotations need to be added where needed. - * @param skipClassDecorators Whether class decorators should be skipped from downleveling. - * This is useful for JIT mode where class decorators should be preserved as they could rely - * on immediate execution. e.g. downleveling `@Injectable` means that the injectable factory - * is not created, and injecting the token will not work. If this decorator would not be - * downleveled, the `Injectable` decorator will execute immediately on file load, and - * Angular will generate the corresponding injectable factory. - */ -export function getDownlevelDecoratorsTransform( - typeChecker: ts.TypeChecker, host: ReflectionHost, diagnostics: ts.Diagnostic[], - isCore: boolean, isClosureCompilerEnabled: boolean, - skipClassDecorators: boolean): ts.TransformerFactory { - function addJSDocTypeAnnotation(node: ts.Node, jsdocType: string): void { - if (!isClosureCompilerEnabled) { - return; - } - - ts.setSyntheticLeadingComments(node, [ - { - kind: ts.SyntaxKind.MultiLineCommentTrivia, - text: `* @type {${jsdocType}} `, - pos: -1, - end: -1, - hasTrailingNewLine: true, - }, - ]); - } - - /** - * Takes a list of decorator metadata object ASTs and produces an AST for a - * static class property of an array of those metadata objects. - */ - function createDecoratorClassProperty(decoratorList: ts.ObjectLiteralExpression[]) { - const modifier = ts.factory.createToken(ts.SyntaxKind.StaticKeyword); - const initializer = ts.factory.createArrayLiteralExpression(decoratorList, true); - // NB: the .decorators property does not get a @nocollapse property. There - // is no good reason why - it means .decorators is not runtime accessible - // if you compile with collapse properties, whereas propDecorators is, - // which doesn't follow any stringent logic. However this has been the - // case previously, and adding it back in leads to substantial code size - // increases as Closure fails to tree shake these props - // without @nocollapse. - const prop = - createPropertyDeclaration([modifier], 'decorators', undefined, undefined, initializer); - addJSDocTypeAnnotation(prop, DECORATOR_INVOCATION_JSDOC_TYPE); - return prop; - } - - /** - * createPropDecoratorsClassProperty creates a static 'propDecorators' - * property containing type information for every property that has a - * decorator applied. - * - * static propDecorators: {[key: string]: {type: Function, args?: - * any[]}[]} = { propA: [{type: MyDecorator, args: [1, 2]}, ...], - * ... - * }; - */ - function createPropDecoratorsClassProperty( - diagnostics: ts.Diagnostic[], - properties: Map): ts.PropertyDeclaration { - // `static propDecorators: {[key: string]: ` + {type: Function, args?: - // any[]}[] + `} = {\n`); - const entries: ts.ObjectLiteralElementLike[] = []; - for (const [name, decorators] of properties.entries()) { - entries.push(ts.factory.createPropertyAssignment( - name, - ts.factory.createArrayLiteralExpression( - decorators.map(deco => extractMetadataFromSingleDecorator(deco, diagnostics))))); - } - const initializer = ts.factory.createObjectLiteralExpression(entries, true); - const prop = createPropertyDeclaration( - [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'propDecorators', undefined, - undefined, initializer); - addJSDocTypeAnnotation(prop, `!Object`); - return prop; - } - - return (context: ts.TransformationContext) => { - // Ensure that referenced type symbols are not elided by TypeScript. Imports for - // such parameter type symbols previously could be type-only, but now might be also - // used in the `ctorParameters` static property as a value. We want to make sure - // that TypeScript does not elide imports for such type references. Read more - // about this in the description for `loadIsReferencedAliasDeclarationPatch`. - const referencedParameterTypes = loadIsReferencedAliasDeclarationPatch(context); - - /** - * Converts an EntityName (from a type annotation) to an expression (accessing a value). - * - * For a given qualified name, this walks depth first to find the leftmost identifier, - * and then converts the path into a property access that can be used as expression. - */ - function entityNameToExpression(name: ts.EntityName): ts.Expression|undefined { - const symbol = typeChecker.getSymbolAtLocation(name); - // Check if the entity name references a symbol that is an actual value. If it is not, it - // cannot be referenced by an expression, so return undefined. - if (!symbol || !symbolIsRuntimeValue(typeChecker, symbol) || !symbol.declarations || - symbol.declarations.length === 0) { - return undefined; - } - // If we deal with a qualified name, build up a property access expression - // that could be used in the JavaScript output. - if (ts.isQualifiedName(name)) { - const containerExpr = entityNameToExpression(name.left); - if (containerExpr === undefined) { - return undefined; - } - return ts.factory.createPropertyAccessExpression(containerExpr, name.right); - } - const decl = symbol.declarations[0]; - // If the given entity name has been resolved to an alias import declaration, - // ensure that the alias declaration is not elided by TypeScript, and use its - // name identifier to reference it at runtime. - if (isAliasImportDeclaration(decl)) { - referencedParameterTypes.add(decl); - // If the entity name resolves to an alias import declaration, we reference the - // entity based on the alias import name. This ensures that TypeScript properly - // resolves the link to the import. Cloning the original entity name identifier - // could lead to an incorrect resolution at local scope. e.g. Consider the following - // snippet: `constructor(Dep: Dep) {}`. In such a case, the local `Dep` identifier - // would resolve to the actual parameter name, and not to the desired import. - // This happens because the entity name identifier symbol is internally considered - // as type-only and therefore TypeScript tries to resolve it as value manually. - // We can help TypeScript and avoid this non-reliable resolution by using an identifier - // that is not type-only and is directly linked to the import alias declaration. - if (decl.name !== undefined) { - return ts.setOriginalNode(ts.factory.createIdentifier(decl.name.text), decl.name); - } - } - // Clone the original entity name identifier so that it can be used to reference - // its value at runtime. This is used when the identifier is resolving to a file - // local declaration (otherwise it would resolve to an alias import declaration). - return ts.setOriginalNode(ts.factory.createIdentifier(name.text), name); - } - - /** - * Transforms a class element. Returns a three tuple of name, transformed element, and - * decorators found. Returns an undefined name if there are no decorators to lower on the - * element, or the element has an exotic name. - */ - function transformClassElement(element: ts.ClassElement): - [string|undefined, ts.ClassElement, ts.Decorator[]] { - element = ts.visitEachChild(element, decoratorDownlevelVisitor, context); - const decoratorsToKeep: ts.Decorator[] = []; - const toLower: ts.Decorator[] = []; - const decorators = host.getDecoratorsOfDeclaration(element) || []; - for (const decorator of decorators) { - // We only deal with concrete nodes in TypeScript sources, so we don't - // need to handle synthetically created decorators. - const decoratorNode = decorator.node! as ts.Decorator; - if (!isAngularDecorator(decorator, isCore)) { - decoratorsToKeep.push(decoratorNode); - continue; - } - toLower.push(decoratorNode); - } - if (!toLower.length) return [undefined, element, []]; - - if (!element.name || !ts.isIdentifier(element.name)) { - // Method has a weird name, e.g. - // [Symbol.foo]() {...} - diagnostics.push({ - file: element.getSourceFile(), - start: element.getStart(), - length: element.getEnd() - element.getStart(), - messageText: `Cannot process decorators for class element with non-analyzable name.`, - category: ts.DiagnosticCategory.Error, - code: 0, - }); - return [undefined, element, []]; - } - - const modifiers = decoratorsToKeep.length ? - ts.setTextRange( - ts.factory.createNodeArray(combineModifiers(decoratorsToKeep, getModifiers(element))), - (element as any).modifiers) : - getModifiers(element); - - return [element.name.text, cloneClassElementWithModifiers(element, modifiers), toLower]; - } - - /** - * Transforms a constructor. Returns the transformed constructor and the list of parameter - * information collected, consisting of decorators and optional type. - */ - function transformConstructor(ctor: ts.ConstructorDeclaration): - [ts.ConstructorDeclaration, ParameterDecorationInfo[]] { - ctor = ts.visitEachChild(ctor, decoratorDownlevelVisitor, context); - - const newParameters: ts.ParameterDeclaration[] = []; - const oldParameters = ctor.parameters; - const parametersInfo: ParameterDecorationInfo[] = []; - - for (const param of oldParameters) { - const decoratorsToKeep: ts.Decorator[] = []; - const paramInfo: ParameterDecorationInfo = {decorators: [], type: null}; - const decorators = host.getDecoratorsOfDeclaration(param) || []; - - for (const decorator of decorators) { - // We only deal with concrete nodes in TypeScript sources, so we don't - // need to handle synthetically created decorators. - const decoratorNode = decorator.node! as ts.Decorator; - if (!isAngularDecorator(decorator, isCore)) { - decoratorsToKeep.push(decoratorNode); - continue; - } - paramInfo!.decorators.push(decoratorNode); - } - if (param.type) { - // param has a type provided, e.g. "foo: Bar". - // The type will be emitted as a value expression in entityNameToExpression, which takes - // care not to emit anything for types that cannot be expressed as a value (e.g. - // interfaces). - paramInfo!.type = param.type; - } - parametersInfo.push(paramInfo); - const newParam = updateParameterDeclaration( - param, - combineModifiers( - // Must pass 'undefined' to avoid emitting decorator metadata. - decoratorsToKeep.length ? decoratorsToKeep : undefined, getModifiers(param)), - param.dotDotDotToken, param.name, param.questionToken, param.type, param.initializer); - newParameters.push(newParam); - } - const updated = - updateConstructorDeclaration(ctor, getModifiers(ctor), newParameters, ctor.body); - return [updated, parametersInfo]; - } - - /** - * Transforms a single class declaration: - * - dispatches to strip decorators on members - * - converts decorators on the class to annotations - * - creates a ctorParameters property - * - creates a propDecorators property - */ - function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration { - const newMembers: ts.ClassElement[] = []; - const decoratedProperties = new Map(); - let classParameters: ParameterDecorationInfo[]|null = null; - - for (const member of classDecl.members) { - switch (member.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.MethodDeclaration: { - const [name, newMember, decorators] = transformClassElement(member); - newMembers.push(newMember); - if (name) decoratedProperties.set(name, decorators); - continue; - } - case ts.SyntaxKind.Constructor: { - const ctor = member as ts.ConstructorDeclaration; - if (!ctor.body) break; - const [newMember, parametersInfo] = - transformConstructor(member as ts.ConstructorDeclaration); - classParameters = parametersInfo; - newMembers.push(newMember); - continue; - } - default: - break; - } - newMembers.push(ts.visitEachChild(member, decoratorDownlevelVisitor, context)); - } - - // The `ReflectionHost.getDecoratorsOfDeclaration()` method will not return certain kinds of - // decorators that will never be Angular decorators. So we cannot rely on it to capture all - // the decorators that should be kept. Instead we start off with a set of the raw decorators - // on the class, and only remove the ones that have been identified for downleveling. - const decoratorsToKeep = new Set(getDecorators(classDecl)); - const possibleAngularDecorators = host.getDecoratorsOfDeclaration(classDecl) || []; - - let hasAngularDecorator = false; - const decoratorsToLower = []; - for (const decorator of possibleAngularDecorators) { - // We only deal with concrete nodes in TypeScript sources, so we don't - // need to handle synthetically created decorators. - const decoratorNode = decorator.node! as ts.Decorator; - const isNgDecorator = isAngularDecorator(decorator, isCore); - - // Keep track if we come across an Angular class decorator. This is used - // to determine whether constructor parameters should be captured or not. - if (isNgDecorator) { - hasAngularDecorator = true; - } - - if (isNgDecorator && !skipClassDecorators) { - decoratorsToLower.push(extractMetadataFromSingleDecorator(decoratorNode, diagnostics)); - decoratorsToKeep.delete(decoratorNode); - } - } - - if (decoratorsToLower.length) { - newMembers.push(createDecoratorClassProperty(decoratorsToLower)); - } - if (classParameters) { - if (hasAngularDecorator || classParameters.some(p => !!p.decorators.length)) { - // Capture constructor parameters if the class has Angular decorator applied, - // or if any of the parameters has decorators applied directly. - newMembers.push(createCtorParametersClassProperty( - diagnostics, entityNameToExpression, classParameters, isClosureCompilerEnabled)); - } - } - if (decoratedProperties.size) { - newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties)); - } - - const members = ts.setTextRange( - ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), - classDecl.members); - - return updateClassDeclaration( - classDecl, - combineModifiers( - decoratorsToKeep.size ? Array.from(decoratorsToKeep) : undefined, - getModifiers(classDecl)), - classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, members); - } - - /** - * Transformer visitor that looks for Angular decorators and replaces them with - * downleveled static properties. Also collects constructor type metadata for - * class declaration that are decorated with an Angular decorator. - */ - function decoratorDownlevelVisitor(node: ts.Node): ts.Node { - if (ts.isClassDeclaration(node)) { - return transformClassDeclaration(node); - } - return ts.visitEachChild(node, decoratorDownlevelVisitor, context); - } - - return (sf: ts.SourceFile) => { - // Downlevel decorators and constructor parameter types. We will keep track of all - // referenced constructor parameter types so that we can instruct TypeScript to - // not elide their imports if they previously were only type-only. - return ts.visitEachChild(sf, decoratorDownlevelVisitor, context); - }; - }; -} - -function cloneClassElementWithModifiers( - node: ts.ClassElement, modifiers: readonly ModifierLike[]|undefined): ts.ClassElement { - let clone: ts.ClassElement; - - if (ts.isMethodDeclaration(node)) { - clone = createMethodDeclaration( - modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, - node.parameters, node.type, node.body); - } else if (ts.isPropertyDeclaration(node)) { - clone = createPropertyDeclaration( - modifiers, node.name, node.questionToken, node.type, node.initializer); - } else if (ts.isGetAccessor(node)) { - clone = - createGetAccessorDeclaration(modifiers, node.name, node.parameters, node.type, node.body); - } else if (ts.isSetAccessor(node)) { - clone = createSetAccessorDeclaration(modifiers, node.name, node.parameters, node.body); - } else { - throw new Error(`Unsupported decorated member with kind ${ts.SyntaxKind[node.kind]}`); - } - - return ts.setOriginalNode(clone, node); -} diff --git a/src/transformers/downlevel_decorators_transform/index.ts b/src/transformers/downlevel_decorators_transform/index.ts deleted file mode 100644 index e0ee891b17..0000000000 --- a/src/transformers/downlevel_decorators_transform/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {getDownlevelDecoratorsTransform} from './downlevel_decorators_transform'; diff --git a/src/transformers/downlevel_decorators_transform/patch_alias_reference_resolution.ts b/src/transformers/downlevel_decorators_transform/patch_alias_reference_resolution.ts deleted file mode 100644 index 6491f93a88..0000000000 --- a/src/transformers/downlevel_decorators_transform/patch_alias_reference_resolution.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; - -/** - * Describes a TypeScript transformation context with the internal emit - * resolver exposed. There are requests upstream in TypeScript to expose - * that as public API: https://github.com/microsoft/TypeScript/issues/17516.. - */ -interface TransformationContextWithResolver extends ts.TransformationContext { - getEmitResolver: () => EmitResolver; -} - -const patchedReferencedAliasesSymbol = Symbol('patchedReferencedAliases'); - -/** Describes a subset of the TypeScript internal emit resolver. */ -interface EmitResolver { - isReferencedAliasDeclaration?(node: ts.Node, ...args: unknown[]): void; - [patchedReferencedAliasesSymbol]?: Set; -} - -/** - * Patches the alias declaration reference resolution for a given transformation context - * so that TypeScript knows about the specified alias declarations being referenced. - * - * This exists because TypeScript performs analysis of import usage before transformers - * run and doesn't refresh its state after transformations. This means that imports - * for symbols used as constructor types are elided due to their original type-only usage. - * - * In reality though, since we downlevel decorators and constructor parameters, we want - * these symbols to be retained in the JavaScript output as they will be used as values - * at runtime. We can instruct TypeScript to preserve imports for such identifiers by - * creating a mutable clone of a given import specifier/clause or namespace, but that - * has the downside of preserving the full import in the JS output. See: - * https://github.com/microsoft/TypeScript/blob/3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/src/compiler/transformers/ts.ts#L242-L250. - * - * This is a trick the CLI used in the past for constructor parameter downleveling in JIT: - * https://github.com/angular/angular-cli/blob/b3f84cc5184337666ce61c07b7b9df418030106f/packages/ngtools/webpack/src/transformers/ctor-parameters.ts#L323-L325 - * The trick is not ideal though as it preserves the full import (as outlined before), and it - * results in a slow-down due to the type checker being involved multiple times. The CLI worked - * around this import preserving issue by having another complex post-process step that detects and - * elides unused imports. Note that these unused imports could cause unused chunks being generated - * by Webpack if the application or library is not marked as side-effect free. - * - * This is not ideal though, as we basically re-implement the complex import usage resolution - * from TypeScript. We can do better by letting TypeScript do the import eliding, but providing - * information about the alias declarations (e.g. import specifiers) that should not be elided - * because they are actually referenced (as they will now appear in static properties). - * - * More information about these limitations with transformers can be found in: - * 1. https://github.com/Microsoft/TypeScript/issues/17552. - * 2. https://github.com/microsoft/TypeScript/issues/17516. - * 3. https://github.com/angular/tsickle/issues/635. - * - * The patch we apply to tell TypeScript about actual referenced aliases (i.e. imported symbols), - * matches conceptually with the logic that runs internally in TypeScript when the - * `emitDecoratorMetadata` flag is enabled. TypeScript basically surfaces the same problem and - * solves it conceptually the same way, but obviously doesn't need to access an `@internal` API. - * - * The set that is returned by this function is meant to be filled with import declaration nodes - * that have been referenced in a value-position by the transform, such the installed patch can - * ensure that those import declarations are not elided. - * - * See below. Note that this uses sourcegraph as the TypeScript checker file doesn't display on - * Github. - * https://sourcegraph.com/github.com/microsoft/TypeScript@3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/-/blob/src/compiler/checker.ts#L31219-31257 - */ -export function loadIsReferencedAliasDeclarationPatch(context: ts.TransformationContext): - Set { - // If the `getEmitResolver` method is not available, TS most likely changed the - // internal structure of the transformation context. We will abort gracefully. - if (!isTransformationContextWithEmitResolver(context)) { - throwIncompatibleTransformationContextError(); - } - const emitResolver = context.getEmitResolver(); - - // The emit resolver may have been patched already, in which case we return the set of referenced - // aliases that was created when the patch was first applied. - // See https://github.com/angular/angular/issues/40276. - const existingReferencedAliases = emitResolver[patchedReferencedAliasesSymbol]; - if (existingReferencedAliases !== undefined) { - return existingReferencedAliases; - } - - const originalIsReferencedAliasDeclaration = emitResolver.isReferencedAliasDeclaration; - // If the emit resolver does not have a function called `isReferencedAliasDeclaration`, then - // we abort gracefully as most likely TS changed the internal structure of the emit resolver. - if (originalIsReferencedAliasDeclaration === undefined) { - throwIncompatibleTransformationContextError(); - } - - const referencedAliases = new Set(); - emitResolver.isReferencedAliasDeclaration = function(node, ...args) { - if (isAliasImportDeclaration(node) && referencedAliases.has(node)) { - return true; - } - return originalIsReferencedAliasDeclaration.call(emitResolver, node, ...args); - }; - return emitResolver[patchedReferencedAliasesSymbol] = referencedAliases; -} - -/** - * Gets whether a given node corresponds to an import alias declaration. Alias - * declarations can be import specifiers, namespace imports or import clauses - * as these do not declare an actual symbol but just point to a target declaration. - */ -export function isAliasImportDeclaration(node: ts.Node): node is ts.ImportSpecifier| - ts.NamespaceImport|ts.ImportClause { - return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node); -} - -/** Whether the transformation context exposes its emit resolver. */ -function isTransformationContextWithEmitResolver(context: ts.TransformationContext): - context is TransformationContextWithResolver { - return (context as Partial).getEmitResolver !== undefined; -} - - -/** - * Throws an error about an incompatible TypeScript version for which the alias - * declaration reference resolution could not be monkey-patched. The error will - * also propose potential solutions that can be applied by developers. - */ -function throwIncompatibleTransformationContextError(): never { - throw Error( - 'Unable to downlevel Angular decorators due to an incompatible TypeScript ' + - 'version.\nIf you recently updated TypeScript and this issue surfaces now, consider ' + - 'downgrading.\n\n' + - 'Please report an issue on the Angular repositories when this issue ' + - 'surfaces and you are using a supposedly compatible TypeScript version.'); -} diff --git a/src/transformers/esm_interop_inject.cjs b/src/transformers/esm_interop_inject.cjs new file mode 100644 index 0000000000..48c4651218 --- /dev/null +++ b/src/transformers/esm_interop_inject.cjs @@ -0,0 +1,7 @@ +/** + * @fileoverview This file will be inlined when generating the CommonJS bundle + * for the transformers. ESBuild is not able to transform `import.meta.url` ESM + * usages directly, so we define `import.meta.url` using its CommonJS variant. + */ + +export const import_meta_url = require('url').pathToFileURL(__filename); diff --git a/src/transformers/jit_transform.d.ts b/src/transformers/jit_transform.d.ts new file mode 100644 index 0000000000..e371c398dc --- /dev/null +++ b/src/transformers/jit_transform.d.ts @@ -0,0 +1,10 @@ +/** + * @fileoverview This file will be replaced as a build step with a bundled + * CommonJS version of the Angular JIT transform. + * + * This is necessary because the Jest preset is shipped as CommonJS, while the Angular + * compiler CLI is a strict ESM package that would otherwise require migration to ESM + * of the preset, or result in asynchronous ESM/CJS interops being necessary. + */ + +export { angularJitApplicationTransform } from '@angular/compiler-cli'; diff --git a/src/transformers/replace-resources.ts b/src/transformers/replace-resources.ts index 7c3be030ae..697ab242eb 100644 --- a/src/transformers/replace-resources.ts +++ b/src/transformers/replace-resources.ts @@ -9,7 +9,6 @@ import type { TsCompilerInstance } from 'ts-jest'; import ts from 'typescript'; import { STYLES, STYLE_URLS, TEMPLATE_URL, TEMPLATE, REQUIRE, COMPONENT, STYLE_URL } from '../constants'; -import { getDecorators, getModifiers, toMutableArray } from '../ngtsc/ts_compatibility'; const isAfterVersion = (targetMajor: number, targetMinor: number): boolean => { const [major, minor] = ts.versionMajorMinor.split('.').map((part) => parseInt(part)); @@ -119,8 +118,8 @@ function visitClassDeclaration( } }); } else { - decorators = toMutableArray(getDecorators(node)); - modifiers = toMutableArray(getModifiers(node)); + decorators = [...(ts.getDecorators(node) ?? [])]; + modifiers = [...(ts.getModifiers(node) ?? [])]; } if (!decorators || !decorators.length) { diff --git a/tsconfig.build.json b/tsconfig.build.json index 49ef792a15..898508c36f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,5 +7,5 @@ "stripInternal": true }, "include": ["src"], - "exclude": ["**/*.spec.ts"] + "exclude": ["**/*.spec.ts", "src/transformers/jit_transform.js"] } From 842b031e915c8813e9b33c1574f7bb089ea83e8a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 9 Feb 2024 14:26:22 +0000 Subject: [PATCH 2/3] fixup! feat: support signal inputs, queries and model Skip lint of generated file --- .eslintignore | 3 +-- .prettierignore | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index 477fb2495a..0437664fa8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,5 +4,4 @@ node_modules src/config/setup-jest.ts coverage website -src/transformers/downlevel_decorators_transform -src/ngtsc +src/transformers/jit_transform.js diff --git a/.prettierignore b/.prettierignore index ce8433833e..50fcdee8ed 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ node_modules/ src/**/__snapshots__/ e2e/__tests__/__snapshots__/ CHANGELOG.md +src/transformers/jit_transform.js From cdca0efe6e5ad72910a9e17419e37dc7bcfb1566 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 9 Feb 2024 19:47:12 +0000 Subject: [PATCH 3/3] fixup! feat: support signal inputs, queries and model Decouple TypeScript versions to ensure proper SyntaxKind checks --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf8e16f569..059a77ac5f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "testing" ], "scripts": { - "build-transformers-bundle": "esbuild --bundle --minify src/transformers/jit_transform.d.ts --platform=node --outfile=./src/transformers/jit_transform.js --format=cjs --define:import.meta.url=import_meta_url --inject:./src/transformers/esm_interop_inject.cjs && cp src/transformers/jit_transform.js build/transformers/jit_transform.js", + "build-transformers-bundle": "esbuild --bundle src/transformers/jit_transform.d.ts --platform=node --external:typescript --outfile=./src/transformers/jit_transform.js --format=cjs --define:import.meta.url=import_meta_url --inject:./src/transformers/esm_interop_inject.cjs && cp src/transformers/jit_transform.js build/transformers/jit_transform.js", "build": "tsc -p tsconfig.build.json && yarn build-transformers-bundle", "lint": "eslint --ext .js,.ts .", "lint-fix": "eslint --fix --ext .js,.ts .",