7
7
*/
8
8
9
9
import ts from 'typescript' ;
10
- import { isAccessedViaThis , isInlineFunction , parameterDeclaresProperty } from './analysis' ;
10
+ import {
11
+ isAccessedViaThis ,
12
+ isInlineFunction ,
13
+ MigrationOptions ,
14
+ parameterDeclaresProperty ,
15
+ } from './analysis' ;
11
16
12
17
/** Property that is a candidate to be combined. */
13
18
interface CombineCandidate {
@@ -40,6 +45,7 @@ export function findUninitializedPropertiesToCombine(
40
45
node : ts . ClassDeclaration ,
41
46
constructor : ts . ConstructorDeclaration ,
42
47
localTypeChecker : ts . TypeChecker ,
48
+ options : MigrationOptions ,
43
49
) : {
44
50
toCombine : CombineCandidate [ ] ;
45
51
toHoist : ts . PropertyDeclaration [ ] ;
@@ -67,11 +73,15 @@ export function findUninitializedPropertiesToCombine(
67
73
return null ;
68
74
}
69
75
76
+ const inlinableParameters = options . _internalReplaceParameterReferencesInInitializers
77
+ ? findInlinableParameterReferences ( constructor , localTypeChecker )
78
+ : new Set < ts . Declaration > ( ) ;
79
+
70
80
for ( const [ name , decl ] of membersToDeclarations . entries ( ) ) {
71
81
if ( memberInitializers . has ( name ) ) {
72
82
const initializer = memberInitializers . get ( name ) ! ;
73
83
74
- if ( ! hasLocalReferences ( initializer , constructor , localTypeChecker ) ) {
84
+ if ( ! hasLocalReferences ( initializer , constructor , inlinableParameters , localTypeChecker ) ) {
75
85
toCombine ??= [ ] ;
76
86
toCombine . push ( { declaration : membersToDeclarations . get ( name ) ! , initializer} ) ;
77
87
}
@@ -230,6 +240,87 @@ function getMemberInitializers(constructor: ts.ConstructorDeclaration) {
230
240
return memberInitializers ;
231
241
}
232
242
243
+ /**
244
+ * Checks if the node is an identifier that references a property from the given
245
+ * list. Returns the property if it is.
246
+ */
247
+ function getIdentifierReferencingProperty (
248
+ node : ts . Node ,
249
+ localTypeChecker : ts . TypeChecker ,
250
+ propertyNames : Set < string > ,
251
+ properties : Set < ts . Declaration > ,
252
+ ) : ts . ParameterDeclaration | undefined {
253
+ if ( ! ts . isIdentifier ( node ) || ! propertyNames . has ( node . text ) ) {
254
+ return undefined ;
255
+ }
256
+ const declarations = localTypeChecker . getSymbolAtLocation ( node ) ?. declarations ;
257
+ if ( ! declarations ) {
258
+ return undefined ;
259
+ }
260
+
261
+ for ( const decl of declarations ) {
262
+ if ( properties . has ( decl ) ) {
263
+ return decl as ts . ParameterDeclaration ;
264
+ }
265
+ }
266
+ return undefined ;
267
+ }
268
+
269
+ /**
270
+ * Returns true if the node introduces a new `this` scope (so we can't
271
+ * reference the outer this).
272
+ */
273
+ function introducesNewThisScope ( node : ts . Node ) : boolean {
274
+ return (
275
+ ts . isFunctionDeclaration ( node ) ||
276
+ ts . isFunctionExpression ( node ) ||
277
+ ts . isMethodDeclaration ( node ) ||
278
+ ts . isClassDeclaration ( node ) ||
279
+ ts . isClassExpression ( node )
280
+ ) ;
281
+ }
282
+
283
+ /**
284
+ * Finds constructor parameter references which can be inlined as `this.prop`.
285
+ * - prop must be a readonly property
286
+ * - the reference can't be in a nested function where `this` might refer
287
+ * to something else
288
+ */
289
+ function findInlinableParameterReferences (
290
+ constructorDeclaration : ts . ConstructorDeclaration ,
291
+ localTypeChecker : ts . TypeChecker ,
292
+ ) : Set < ts . Declaration > {
293
+ const eligibleProperties = constructorDeclaration . parameters . filter (
294
+ ( p ) =>
295
+ ts . isIdentifier ( p . name ) && p . modifiers ?. some ( ( s ) => s . kind === ts . SyntaxKind . ReadonlyKeyword ) ,
296
+ ) ;
297
+ const eligibleNames = new Set ( eligibleProperties . map ( ( p ) => ( p . name as ts . Identifier ) . text ) ) ;
298
+ const eligiblePropertiesSet : Set < ts . Declaration > = new Set ( eligibleProperties ) ;
299
+
300
+ function walk ( node : ts . Node , canReferenceThis : boolean ) {
301
+ const property = getIdentifierReferencingProperty (
302
+ node ,
303
+ localTypeChecker ,
304
+ eligibleNames ,
305
+ eligiblePropertiesSet ,
306
+ ) ;
307
+ if ( property && ! canReferenceThis ) {
308
+ // The property is referenced in a nested context where
309
+ // we can't use `this`, so we can't inline it.
310
+ eligiblePropertiesSet . delete ( property ) ;
311
+ } else if ( introducesNewThisScope ( node ) ) {
312
+ canReferenceThis = false ;
313
+ }
314
+
315
+ ts . forEachChild ( node , ( child ) => {
316
+ walk ( child , canReferenceThis ) ;
317
+ } ) ;
318
+ }
319
+
320
+ walk ( constructorDeclaration , true ) ;
321
+ return eligiblePropertiesSet ;
322
+ }
323
+
233
324
/**
234
325
* Determines if a node has references to local symbols defined in the constructor.
235
326
* @param root Expression to check for local references.
@@ -239,6 +330,7 @@ function getMemberInitializers(constructor: ts.ConstructorDeclaration) {
239
330
function hasLocalReferences (
240
331
root : ts . Expression ,
241
332
constructor : ts . ConstructorDeclaration ,
333
+ allowedParameters : Set < ts . Declaration > ,
242
334
localTypeChecker : ts . TypeChecker ,
243
335
) : boolean {
244
336
const sourceFile = root . getSourceFile ( ) ;
@@ -265,6 +357,7 @@ function hasLocalReferences(
265
357
// The source file check is a bit redundant since the type checker
266
358
// is local to the file, but it's inexpensive and it can prevent
267
359
// bugs in the future if we decide to use a full type checker.
360
+ ! allowedParameters . has ( decl ) &&
268
361
decl . getSourceFile ( ) === sourceFile &&
269
362
decl . getStart ( ) >= constructor . getStart ( ) &&
270
363
decl . getEnd ( ) <= constructor . getEnd ( ) &&
0 commit comments