Skip to content

Commit ddb95c5

Browse files
crisbetoalxhub
authored andcommitted
refactor(compiler): replace most usages of getMutableClone (#47167)
Replaces (almost) all of the usages of the deprecated `getMutableClone` function from TypeScript which has started to log deprecation warnings in version 4.8 and will likely be removed in version 5.0. The one place we have left is in the default import handling of ngtsc which will be more difficult to remove. PR Close #47167
1 parent fc97a41 commit ddb95c5

File tree

10 files changed

+162
-58
lines changed

10 files changed

+162
-58
lines changed

packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,12 @@ function decoratorToMetadata(
137137
}
138138
// Decorators have a type.
139139
const properties: ts.ObjectLiteralElementLike[] = [
140-
ts.factory.createPropertyAssignment('type', ts.getMutableClone(decorator.identifier)),
140+
ts.factory.createPropertyAssignment('type', decorator.identifier),
141141
];
142142
// Sometimes they have arguments.
143143
if (decorator.args !== null && decorator.args.length > 0) {
144144
const args = decorator.args.map(arg => {
145-
const expr = ts.getMutableClone(arg);
146-
return wrapFunctionsInParens ? wrapFunctionExpressionsInParens(expr) : expr;
145+
return wrapFunctionsInParens ? wrapFunctionExpressionsInParens(arg) : arg;
147146
});
148147
properties.push(
149148
ts.factory.createPropertyAssignment('args', ts.factory.createArrayLiteralExpression(args)));

packages/compiler-cli/src/ngtsc/imports/src/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export class DefaultImportTracker {
138138
//
139139
// TODO(alxhub): discuss with the TypeScript team and determine if there's a better way to
140140
// deal with this issue.
141+
// tslint:disable-next-line: ban
141142
stmt = ts.getMutableClone(stmt);
142143
}
143144
return stmt;

packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ function entityNameToValue(node: ts.EntityName): ts.Expression|null {
267267
const left = entityNameToValue(node.left);
268268
return left !== null ? ts.factory.createPropertyAccessExpression(left, node.right) : null;
269269
} else if (ts.isIdentifier(node)) {
270-
return ts.getMutableClone(node);
270+
const clone = ts.setOriginalNode(ts.factory.createIdentifier(node.text), node);
271+
(clone as any).parent = node.parent;
272+
return clone;
271273
} else {
272274
return null;
273275
}

packages/compiler-cli/src/ngtsc/ts_compatibility/src/ts_cross_version_utils.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import ts from 'typescript';
1010

1111
/** Whether the current TypeScript version is after 4.8. */
12-
export const IS_AFTER_TS_48 = isAfterVersion(4, 8);
12+
const IS_AFTER_TS_48 = isAfterVersion(4, 8);
1313

1414
/** Whether the current TypeScript version is after 4.7. */
1515
const IS_AFTER_TS_47 = isAfterVersion(4, 7);
@@ -91,6 +91,24 @@ export const updateClassDeclaration: Ts48UpdateClassDeclarationFn = IS_AFTER_TS_
9191
ts.factory.updateClassDeclaration as any)(
9292
node, ...splitModifiers(combinedModifiers), name, typeParameters, heritageClauses, members);
9393

94+
/** Type of `ts.factory.createClassDeclaration` in TS 4.8+. */
95+
type Ts48CreateClassDeclarationFn =
96+
(modifiers: readonly ModifierLike[]|undefined, name: ts.Identifier|undefined,
97+
typeParameters: readonly ts.TypeParameterDeclaration[]|undefined,
98+
heritageClauses: readonly ts.HeritageClause[]|undefined,
99+
members: readonly ts.ClassElement[]) => ts.ClassDeclaration;
100+
101+
/**
102+
* Creates a `ts.ClassDeclaration` declaration.
103+
*
104+
* TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8.
105+
* We should remove it once we have dropped support for the older versions.
106+
*/
107+
export const createClassDeclaration: Ts48CreateClassDeclarationFn = IS_AFTER_TS_48 ?
108+
(ts.factory.createClassDeclaration as any) :
109+
(combinedModifiers, name, typeParameters, heritageClauses, members) =>
110+
(ts.factory.createClassDeclaration as any)(
111+
...splitModifiers(combinedModifiers), name, typeParameters, heritageClauses, members);
94112

95113
/** Type of `ts.factory.updateMethodDeclaration` in TS 4.8+. */
96114
type Ts48UpdateMethodDeclarationFn =
@@ -114,6 +132,26 @@ export const updateMethodDeclaration: Ts48UpdateMethodDeclarationFn = IS_AFTER_T
114132
node, ...splitModifiers(modifiers), asteriskToken, name, questionToken, typeParameters,
115133
parameters, type, body);
116134

135+
/** Type of `ts.factory.createMethodDeclaration` in TS 4.8+. */
136+
type Ts48CreateMethodDeclarationFn =
137+
(modifiers: readonly ModifierLike[]|undefined, asteriskToken: ts.AsteriskToken|undefined,
138+
name: ts.PropertyName, questionToken: ts.QuestionToken|undefined,
139+
typeParameters: readonly ts.TypeParameterDeclaration[]|undefined,
140+
parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode|undefined,
141+
body: ts.Block|undefined) => ts.MethodDeclaration;
142+
143+
/**
144+
* Creates a `ts.MethodDeclaration` declaration.
145+
*
146+
* TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8.
147+
* We should remove it once we have dropped support for the older versions.
148+
*/
149+
export const createMethodDeclaration: Ts48CreateMethodDeclarationFn = IS_AFTER_TS_48 ?
150+
(ts.factory.createMethodDeclaration as any) :
151+
(modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body) =>
152+
(ts.factory.createMethodDeclaration as any)(
153+
...splitModifiers(modifiers), asteriskToken, name, questionToken, typeParameters,
154+
parameters, type, body);
117155

118156
/** Type of `ts.factory.updatePropertyDeclaration` in TS 4.8+. */
119157
type Ts48UpdatePropertyDeclarationFn =
@@ -153,15 +191,12 @@ export const createPropertyDeclaration: Ts48CreatePropertyDeclarationFn = IS_AFT
153191
(ts.factory.createPropertyDeclaration as any)(
154192
...splitModifiers(modifiers), name, questionOrExclamationToken, type, initializer);
155193

156-
157-
158194
/** Type of `ts.factory.updateGetAccessorDeclaration` in TS 4.8+. */
159195
type Ts48UpdateGetAccessorDeclarationFn =
160196
(node: ts.GetAccessorDeclaration, modifiers: readonly ModifierLike[]|undefined,
161197
name: ts.PropertyName, parameters: readonly ts.ParameterDeclaration[],
162198
type: ts.TypeNode|undefined, body: ts.Block|undefined) => ts.GetAccessorDeclaration;
163199

164-
165200
/**
166201
* Updates a `ts.GetAccessorDeclaration` declaration.
167202
*
@@ -174,6 +209,23 @@ export const updateGetAccessorDeclaration: Ts48UpdateGetAccessorDeclarationFn =
174209
(ts.factory.updateGetAccessorDeclaration as any)(
175210
node, ...splitModifiers(modifiers), name, parameters, type, body);
176211

212+
/** Type of `ts.factory.createGetAccessorDeclaration` in TS 4.8+. */
213+
type Ts48CreateGetAccessorDeclarationFn =
214+
(modifiers: readonly ModifierLike[]|undefined, name: ts.PropertyName,
215+
parameters: readonly ts.ParameterDeclaration[], type: ts.TypeNode|undefined,
216+
body: ts.Block|undefined) => ts.GetAccessorDeclaration;
217+
218+
/**
219+
* Creates a `ts.GetAccessorDeclaration` declaration.
220+
*
221+
* TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8.
222+
* We should remove it once we have dropped support for the older versions.
223+
*/
224+
export const createGetAccessorDeclaration: Ts48CreateGetAccessorDeclarationFn = IS_AFTER_TS_48 ?
225+
(ts.factory.createGetAccessorDeclaration as any) :
226+
(modifiers, name, parameters, type, body) => (ts.factory.createGetAccessorDeclaration as any)(
227+
...splitModifiers(modifiers), name, parameters, type, body);
228+
177229
/** Type of `ts.factory.updateSetAccessorDeclaration` in TS 4.8+. */
178230
type Ts48UpdateSetAccessorDeclarationFn =
179231
(node: ts.SetAccessorDeclaration, modifiers: readonly ModifierLike[]|undefined,
@@ -191,7 +243,22 @@ export const updateSetAccessorDeclaration: Ts48UpdateSetAccessorDeclarationFn =
191243
(node, modifiers, name, parameters, body) => (ts.factory.updateSetAccessorDeclaration as any)(
192244
node, ...splitModifiers(modifiers), name, parameters, body);
193245

246+
/** Type of `ts.factory.createSetAccessorDeclaration` in TS 4.8+. */
247+
type Ts48CreateSetAccessorDeclarationFn =
248+
(modifiers: readonly ModifierLike[]|undefined, name: ts.PropertyName,
249+
parameters: readonly ts.ParameterDeclaration[], body: ts.Block|undefined) =>
250+
ts.SetAccessorDeclaration;
194251

252+
/**
253+
* Creates a `ts.GetAccessorDeclaration` declaration.
254+
*
255+
* TODO(crisbeto): this is a backwards-compatibility layer for versions of TypeScript less than 4.8.
256+
* We should remove it once we have dropped support for the older versions.
257+
*/
258+
export const createSetAccessorDeclaration: Ts48CreateSetAccessorDeclarationFn = IS_AFTER_TS_48 ?
259+
(ts.factory.createSetAccessorDeclaration as any) :
260+
(modifiers, name, parameters, body) => (ts.factory.createSetAccessorDeclaration as any)(
261+
...splitModifiers(modifiers), name, parameters, body);
195262

196263
/** Type of `ts.factory.updateConstructorDeclaration` in TS 4.8+. */
197264
type Ts48UpdateConstructorDeclarationFn =

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ class TcbReferenceOp extends TcbOp {
550550

551551
override execute(): ts.Identifier {
552552
const id = this.tcb.allocateId();
553-
let initializer =
553+
let initializer: ts.Expression =
554554
this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement ?
555555
this.scope.resolve(this.target) :
556556
this.scope.resolve(this.host, this.target);
@@ -1305,7 +1305,7 @@ class Scope {
13051305
*/
13061306
resolve(
13071307
node: TmplAstElement|TmplAstTemplate|TmplAstVariable|TmplAstReference,
1308-
directive?: TypeCheckableDirectiveMeta): ts.Expression {
1308+
directive?: TypeCheckableDirectiveMeta): ts.Identifier|ts.NonNullExpression {
13091309
// Attempt to resolve the operation locally.
13101310
const res = this.resolveLocal(node, directive);
13111311
if (res !== null) {
@@ -1317,10 +1317,19 @@ class Scope {
13171317
//
13181318
// In addition, returning a clone prevents the consumer of `Scope#resolve` from
13191319
// attaching comments at the declaration site.
1320+
let clone: ts.Identifier|ts.NonNullExpression;
13201321

1321-
const clone = ts.getMutableClone(res);
1322-
ts.setSyntheticTrailingComments(clone, []);
1323-
return clone;
1322+
if (ts.isIdentifier(res)) {
1323+
clone = ts.factory.createIdentifier(res.text);
1324+
} else if (ts.isNonNullExpression(res)) {
1325+
clone = ts.factory.createNonNullExpression(res.expression);
1326+
} else {
1327+
throw new Error(`Could not resolve ${node} to an Identifier or a NonNullExpression`);
1328+
}
1329+
1330+
ts.setOriginalNode(clone, res);
1331+
(clone as any).parent = clone.parent;
1332+
return ts.setSyntheticTrailingComments(clone, []);
13241333
} else if (this.parent !== null) {
13251334
// Check with the parent.
13261335
return this.parent.resolve(node, directive);

packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,22 @@ export class TypeEmitter {
121121
// issue the literal is cloned and explicitly marked as synthesized by setting its text
122122
// range to a negative range, forcing TypeScript to determine the node's literal text from
123123
// the synthesized node's text instead of the incorrect source file.
124-
const clone = ts.getMutableClone(node);
124+
let clone: ts.LiteralExpression;
125+
126+
if (ts.isStringLiteral(node)) {
127+
clone = ts.factory.createStringLiteral(node.text);
128+
} else if (ts.isNumericLiteral(node)) {
129+
clone = ts.factory.createNumericLiteral(node.text);
130+
} else if (ts.isBigIntLiteral(node)) {
131+
clone = ts.factory.createBigIntLiteral(node.text);
132+
} else if (ts.isNoSubstitutionTemplateLiteral(node)) {
133+
clone = ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
134+
} else if (ts.isRegularExpressionLiteral(node)) {
135+
clone = ts.factory.createRegularExpressionLiteral(node.text);
136+
} else {
137+
throw new Error(`Unsupported literal kind ${ts.SyntaxKind[node.kind]}`);
138+
}
139+
125140
ts.setTextRange(clone, {pos: -1, end: -1});
126141
return clone;
127142
} else {

packages/compiler-cli/src/ngtsc/util/src/visitor.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,27 +90,23 @@ export abstract class Visitor {
9090

9191
// If the visited node has a `statements` array then process them, maybe replacing the visited
9292
// node and adding additional statements.
93-
if (hasStatements(visitedNode)) {
93+
if (ts.isBlock(visitedNode) || ts.isSourceFile(visitedNode)) {
9494
visitedNode = this._maybeProcessStatements(visitedNode);
9595
}
9696

9797
return visitedNode;
9898
}
9999

100-
private _maybeProcessStatements<T extends ts.Node&{statements: ts.NodeArray<ts.Statement>}>(
101-
node: T): T {
100+
private _maybeProcessStatements<T extends ts.Block|ts.SourceFile>(node: T): T {
102101
// Shortcut - if every statement doesn't require nodes to be prepended or appended,
103102
// this is a no-op.
104103
if (node.statements.every(stmt => !this._before.has(stmt) && !this._after.has(stmt))) {
105104
return node;
106105
}
107106

108-
// There are statements to prepend, so clone the original node.
109-
const clone = ts.getMutableClone(node);
110-
111107
// Build a new list of statements and patch it onto the clone.
112108
const newStatements: ts.Statement[] = [];
113-
clone.statements.forEach(stmt => {
109+
node.statements.forEach(stmt => {
114110
if (this._before.has(stmt)) {
115111
newStatements.push(...(this._before.get(stmt)! as ts.Statement[]));
116112
this._before.delete(stmt);
@@ -121,12 +117,17 @@ export abstract class Visitor {
121117
this._after.delete(stmt);
122118
}
123119
});
124-
clone.statements = ts.factory.createNodeArray(newStatements, node.statements.hasTrailingComma);
125-
return clone;
126-
}
127-
}
128120

129-
function hasStatements(node: ts.Node): node is ts.Node&{statements: ts.NodeArray<ts.Statement>} {
130-
const block = node as {statements?: any};
131-
return block.statements !== undefined && Array.isArray(block.statements);
121+
const statementsArray =
122+
ts.factory.createNodeArray(newStatements, node.statements.hasTrailingComma);
123+
124+
if (ts.isBlock(node)) {
125+
return ts.factory.updateBlock(node, statementsArray) as T;
126+
} else {
127+
return ts.factory.updateSourceFile(
128+
node, statementsArray, node.isDeclarationFile, node.referencedFiles,
129+
node.typeReferenceDirectives, node.hasNoDefaultLib, node.libReferenceDirectives) as
130+
T;
131+
}
132+
}
132133
}

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

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import ts from 'typescript';
1010

1111
import {Decorator, ReflectionHost} from '../../ngtsc/reflection';
12-
import {combineModifiers, createPropertyDeclaration, getDecorators, getModifiers, IS_AFTER_TS_48, updateClassDeclaration, updateConstructorDeclaration, updateParameterDeclaration} from '../../ngtsc/ts_compatibility';
12+
import {combineModifiers, createClassDeclaration, createGetAccessorDeclaration, createMethodDeclaration, createPropertyDeclaration, createSetAccessorDeclaration, getDecorators, getModifiers, ModifierLike, updateConstructorDeclaration, updateParameterDeclaration} from '../../ngtsc/ts_compatibility';
1313

1414
import {isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './patch_alias_reference_resolution';
1515

@@ -375,13 +375,13 @@ export function getDownlevelDecoratorsTransform(
375375
// We can help TypeScript and avoid this non-reliable resolution by using an identifier
376376
// that is not type-only and is directly linked to the import alias declaration.
377377
if (decl.name !== undefined) {
378-
return ts.getMutableClone(decl.name);
378+
return ts.setOriginalNode(ts.factory.createIdentifier(decl.name.text), decl.name);
379379
}
380380
}
381381
// Clone the original entity name identifier so that it can be used to reference
382382
// its value at runtime. This is used when the identifier is resolving to a file
383383
// local declaration (otherwise it would resolve to an alias import declaration).
384-
return ts.getMutableClone(name);
384+
return ts.setOriginalNode(ts.factory.createIdentifier(name.text), name);
385385
}
386386

387387
/**
@@ -421,23 +421,13 @@ export function getDownlevelDecoratorsTransform(
421421
return [undefined, element, []];
422422
}
423423

424-
const name = (element.name as ts.Identifier).text;
425-
const mutable = ts.getMutableClone(element);
426-
427-
if (IS_AFTER_TS_48) {
428-
// As of TS 4.8, the decorators are part of the `modifiers` array.
429-
(mutable as any).modifiers = decoratorsToKeep.length ?
430-
ts.setTextRange(
431-
ts.factory.createNodeArray(
432-
combineModifiers(decoratorsToKeep, getModifiers(mutable))),
433-
mutable.modifiers) :
434-
undefined;
435-
} else {
436-
(mutable as any).decorators = decoratorsToKeep.length ?
437-
ts.setTextRange(ts.factory.createNodeArray(decoratorsToKeep), mutable.decorators) :
438-
undefined;
439-
}
440-
return [name, mutable, toLower];
424+
const modifiers = decoratorsToKeep.length ?
425+
ts.setTextRange(
426+
ts.factory.createNodeArray(combineModifiers(decoratorsToKeep, getModifiers(element))),
427+
element.modifiers) :
428+
getModifiers(element);
429+
430+
return [element.name.text, cloneClassElementWithModifiers(element, modifiers), toLower];
441431
}
442432

443433
/**
@@ -496,8 +486,6 @@ export function getDownlevelDecoratorsTransform(
496486
* - creates a propDecorators property
497487
*/
498488
function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration {
499-
classDecl = ts.getMutableClone(classDecl);
500-
501489
const newMembers: ts.ClassElement[] = [];
502490
const decoratedProperties = new Map<string, ts.Decorator[]>();
503491
let classParameters: ParameterDecorationInfo[]|null = null;
@@ -574,8 +562,7 @@ export function getDownlevelDecoratorsTransform(
574562
ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma),
575563
classDecl.members);
576564

577-
return updateClassDeclaration(
578-
classDecl,
565+
return createClassDeclaration(
579566
combineModifiers(
580567
decoratorsToKeep.size ? Array.from(decoratorsToKeep) : undefined,
581568
getModifiers(classDecl)),
@@ -602,3 +589,26 @@ export function getDownlevelDecoratorsTransform(
602589
};
603590
};
604591
}
592+
593+
function cloneClassElementWithModifiers(
594+
node: ts.ClassElement, modifiers: readonly ModifierLike[]|undefined): ts.ClassElement {
595+
let clone: ts.ClassElement;
596+
597+
if (ts.isMethodDeclaration(node)) {
598+
clone = createMethodDeclaration(
599+
modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters,
600+
node.parameters, node.type, node.body);
601+
} else if (ts.isPropertyDeclaration(node)) {
602+
clone = createPropertyDeclaration(
603+
modifiers, node.name, node.questionToken, node.type, node.initializer);
604+
} else if (ts.isGetAccessor(node)) {
605+
clone =
606+
createGetAccessorDeclaration(modifiers, node.name, node.parameters, node.type, node.body);
607+
} else if (ts.isSetAccessor(node)) {
608+
clone = createSetAccessorDeclaration(modifiers, node.name, node.parameters, node.body);
609+
} else {
610+
throw new Error(`Unsupported decorated member with kind ${ts.SyntaxKind[node.kind]}`);
611+
}
612+
613+
return ts.setOriginalNode(clone, node);
614+
}

packages/compiler-cli/test/downlevel_decorators_transform_spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,11 +649,10 @@ describe('downlevel decorator transform', () => {
649649
const stripAllDecoratorsTransform: ts.TransformerFactory<ts.SourceFile> = context => {
650650
return (sourceFile: ts.SourceFile) => {
651651
const visitNode = (node: ts.Node): ts.Node => {
652-
if (ts.isClassDeclaration(node) || ts.isClassElement(node)) {
653-
const cloned = ts.getMutableClone(node);
654-
(cloned.decorators as undefined) = undefined;
655-
(cloned.modifiers as undefined) = undefined;
656-
return cloned;
652+
if (ts.isClassDeclaration(node)) {
653+
return ts.factory.createClassDeclaration(
654+
ts.getModifiers(node), node.name, node.typeParameters, node.heritageClauses,
655+
node.members);
657656
}
658657
return ts.visitEachChild(node, visitNode, context);
659658
};

0 commit comments

Comments
 (0)