Skip to content

Commit e6514b9

Browse files
cexbrayatpkozlowski-opensource
authored andcommitted
fix(migrations): do not migrate next calls in template if not an EventEmitter (#58631)
Fixes #58630 ```html <button (click)="someSubject.next()">Click</button> ``` was migrated to an `.emit` call, even if `someSubject` was not an `EventEmitter`. The issue was the same in host listeners. PR Close #58631
1 parent e33b195 commit e6514b9

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

packages/core/schematics/migrations/output-migration/output-migration.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,40 @@ describe('outputs', () => {
257257
});
258258
});
259259

260+
it('should _not_ migrate .next usages when not an EventEmitter in external template', async () => {
261+
const tsContents = `
262+
import {Component} from '@angular/core';
263+
import {Subject} from 'rxjs';
264+
265+
@Component({
266+
selector: 'test-cmp',
267+
templateUrl: '/app.component.html'
268+
})
269+
export class TestCmpWithTemplate {
270+
someChange = new Subject<void>();
271+
}
272+
`;
273+
const htmlContents = `<button (click)="someChange.next()">click me</button>`;
274+
const {fs} = await runTsurgeMigration(new OutputMigration(), [
275+
{
276+
name: absoluteFrom('/app.component.ts'),
277+
isProgramRootFile: true,
278+
contents: tsContents,
279+
},
280+
{
281+
name: absoluteFrom('/app.component.html'),
282+
contents: htmlContents,
283+
},
284+
]);
285+
286+
const cmpActual = fs.readFile(absoluteFrom('/app.component.ts'));
287+
const htmlActual = fs.readFile(absoluteFrom('/app.component.html'));
288+
289+
// nothing should have changed
290+
expect(cmpActual).withContext(diffText(tsContents, cmpActual)).toEqual(tsContents);
291+
expect(htmlActual).withContext(diffText(htmlContents, htmlActual)).toEqual(htmlContents);
292+
});
293+
260294
it('should migrate .next usage inside external template expressions', async () => {
261295
const {fs} = await runTsurgeMigration(new OutputMigration(), [
262296
{
@@ -333,6 +367,25 @@ describe('outputs', () => {
333367
});
334368
});
335369

370+
it('should _not_ migrate .next usage inside host listeners if not an EventEmitter', async () => {
371+
await verifyNoChange(
372+
`
373+
import {Component, Output, EventEmitter} from '@angular/core';
374+
375+
@Component({
376+
selector: 'test-cmp',
377+
host: {
378+
'(click)': 'someChange.next()'
379+
},
380+
template: ''
381+
})
382+
export class TestCmpWithTemplate {
383+
someChange = new Subject();
384+
}
385+
`,
386+
);
387+
});
388+
336389
it('should migrate .next usage in @HostListener', async () => {
337390
await verify({
338391
before: `

packages/core/schematics/migrations/output-migration/output-migration.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class OutputMigration extends TsurgeFunnelMigration<
112112
const knownFields: KnownFields<ClassFieldDescriptor> = {
113113
// Note: We don't support cross-target migration of `Partial<T>` usages.
114114
// This is an acceptable limitation for performance reasons.
115-
shouldTrackClassReference: (node) => false,
115+
shouldTrackClassReference: () => false,
116116
attemptRetrieveDescriptorFromSymbol: (s) => {
117117
const propDeclaration = getTargetPropertyDeclaration(s);
118118
if (propDeclaration !== null) {
@@ -278,7 +278,11 @@ export class OutputMigration extends TsurgeFunnelMigration<
278278
// detect .next usages that should be migrated to .emit in template and host binding expressions
279279
if (ref.kind === ReferenceKind.InTemplate) {
280280
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
281-
if (callExpr !== null) {
281+
// TODO: here and below for host bindings, we should ideally filter in the global meta stage
282+
// (instead of using the `outputFieldReplacements` map)
283+
// as technically, the call expression could refer to an output
284+
// from a whole different compilation unit (e.g. tsconfig.json).
285+
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
282286
addOutputReplacement(
283287
outputFieldReplacements,
284288
ref.target.key,
@@ -288,7 +292,7 @@ export class OutputMigration extends TsurgeFunnelMigration<
288292
}
289293
} else if (ref.kind === ReferenceKind.InHostBinding) {
290294
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
291-
if (callExpr !== null) {
295+
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
292296
addOutputReplacement(
293297
outputFieldReplacements,
294298
ref.target.key,

0 commit comments

Comments
 (0)