Skip to content

Commit 2b4b7c3

Browse files
crisbetokirjs
authored andcommitted
fix(compiler-cli): handle more node types when extracting dependencies (#59445)
Fixes that the HMR dependency extraction logic wasn't handling some node types. Most of these are a bit edge-case-ey in component definitions, but variable initializers and arrow functions can realistically happen in factories. PR Close #59445
1 parent fb67b10 commit 2b4b7c3

File tree

2 files changed

+101
-9
lines changed

2 files changed

+101
-9
lines changed

packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,7 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
210210
}
211211

212212
// Identifier referenced at the top level. Unlikely.
213-
if (
214-
ts.isSourceFile(parent) ||
215-
(ts.isExpressionStatement(parent) && parent.expression === node)
216-
) {
213+
if (ts.isSourceFile(parent)) {
217214
return true;
218215
}
219216

@@ -225,6 +222,7 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
225222

226223
// Identifier used in a nested expression is only top-level if it's the actual expression.
227224
if (
225+
ts.isExpressionStatement(parent) ||
228226
ts.isPropertyAccessExpression(parent) ||
229227
ts.isComputedPropertyName(parent) ||
230228
ts.isTemplateSpan(parent) ||
@@ -235,8 +233,6 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
235233
ts.isIfStatement(parent) ||
236234
ts.isDoStatement(parent) ||
237235
ts.isWhileStatement(parent) ||
238-
ts.isForInStatement(parent) ||
239-
ts.isForOfStatement(parent) ||
240236
ts.isSwitchStatement(parent) ||
241237
ts.isCaseClause(parent) ||
242238
ts.isThrowStatement(parent)
@@ -249,17 +245,28 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
249245
return parent.elements.includes(node);
250246
}
251247

252-
// Identifier in a property assignment is only top level if it's the initializer.
253-
if (ts.isPropertyAssignment(parent)) {
248+
// If the parent is an initialized node, the identifier is
249+
// at the top level if it's the initializer itself.
250+
if (
251+
ts.isPropertyAssignment(parent) ||
252+
ts.isParameter(parent) ||
253+
ts.isBindingElement(parent) ||
254+
ts.isPropertyDeclaration(parent) ||
255+
ts.isEnumMember(parent)
256+
) {
254257
return parent.initializer === node;
255258
}
256259

260+
// Identifier in a function is top level if it's either the name or the initializer.
261+
if (ts.isVariableDeclaration(parent)) {
262+
return parent.name === node || parent.initializer === node;
263+
}
264+
257265
// Identifier in a declaration is only top level if it's the name.
258266
// In shorthand assignments the name is also the value.
259267
if (
260268
ts.isClassDeclaration(parent) ||
261269
ts.isFunctionDeclaration(parent) ||
262-
ts.isVariableDeclaration(parent) ||
263270
ts.isShorthandPropertyAssignment(parent)
264271
) {
265272
return parent.name === node;
@@ -273,6 +280,20 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
273280
return parent.left === node || parent.right === node;
274281
}
275282

283+
if (ts.isForInStatement(parent) || ts.isForOfStatement(parent)) {
284+
return parent.expression === node || parent.initializer === node;
285+
}
286+
287+
if (ts.isForStatement(parent)) {
288+
return (
289+
parent.condition === node || parent.initializer === node || parent.incrementor === node
290+
);
291+
}
292+
293+
if (ts.isArrowFunction(parent)) {
294+
return parent.body === node;
295+
}
296+
276297
// It's unlikely that we'll run into imports/exports in this use case.
277298
// We handle them since it's simple and for completeness' sake.
278299
if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) {

packages/compiler-cli/test/ngtsc/hmr_spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,5 +431,76 @@ runInEachFileSystem(() => {
431431
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, providers, Component) {',
432432
);
433433
});
434+
435+
it('should capture variable initializer dependencies', () => {
436+
enableHmr();
437+
env.write(
438+
'test.ts',
439+
`
440+
import {Component, InjectionToken} from '@angular/core';
441+
442+
const token = new InjectionToken<number>('TEST');
443+
const value = 123;
444+
445+
@Component({
446+
template: '',
447+
providers: [{
448+
provide: token,
449+
useFactory: () => {
450+
const v = value;
451+
return v;
452+
}
453+
}]
454+
})
455+
export class Cmp {}
456+
`,
457+
);
458+
459+
env.driveMain();
460+
461+
const jsContents = env.getContents('test.js');
462+
const hmrContents = env.driveHmr('test.ts', 'Cmp');
463+
464+
expect(jsContents).toContain(
465+
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component]));',
466+
);
467+
expect(hmrContents).toContain(
468+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, Component) {',
469+
);
470+
});
471+
472+
it('should capture arrow function dependencies', () => {
473+
enableHmr();
474+
env.write(
475+
'test.ts',
476+
`
477+
import {Component, InjectionToken} from '@angular/core';
478+
479+
const token = new InjectionToken<number>('TEST');
480+
const value = 123;
481+
482+
@Component({
483+
template: '',
484+
providers: [{
485+
provide: token,
486+
useFactory: () => value
487+
}]
488+
})
489+
export class Cmp {}
490+
`,
491+
);
492+
493+
env.driveMain();
494+
495+
const jsContents = env.getContents('test.js');
496+
const hmrContents = env.driveHmr('test.ts', 'Cmp');
497+
498+
expect(jsContents).toContain(
499+
'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, Component]));',
500+
);
501+
expect(hmrContents).toContain(
502+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, Component) {',
503+
);
504+
});
434505
});
435506
});

0 commit comments

Comments
 (0)