Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Detect and remove complete calls for migrated outputs #57671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,58 @@ describe('outputs', () => {
this.someChange.next('clicked');
}

someMethod() {
this.someChange.pipe();
}
}
`,
);
});
});

describe('.complete migration', () => {
it('should remove .complete usage for migrated outputs', () => {
verify({
before: `
import {Directive, Output, EventEmitter} from '@angular/core';

@Directive()
export class TestDir {
@Output() someChange = new EventEmitter<string>();

ngOnDestroy() {
this.someChange.complete();
}
}
`,
after: `
import { Directive, output } from '@angular/core';

@Directive()
export class TestDir {
readonly someChange = output<string>();

ngOnDestroy() {

}
}
`,
});
});

it('should _not_ migrate .complete usage outside of expression statements', () => {
verifyNoChange(
`
import {Directive, Output, EventEmitter} from '@angular/core';

@Directive()
export class TestDir {
@Output() someChange = new EventEmitter<string>();

ngOnDestroy() {
this.someChange.complete();
// play it safe and skip replacement for any .complete usage that are not
// trivial expression statements
(this.someChange.complete());
}
}
`,
Expand All @@ -174,18 +224,18 @@ describe('outputs', () => {
`);
});

it('should _not_ migrate outputs that are used with .complete', () => {
it('should _not_ migrate outputs that are used with .pipe outside of a component class', () => {
verifyNoChange(`
import {Directive, Output, EventEmitter, OnDestroy} from '@angular/core';
import {Directive, Output, EventEmitter} from '@angular/core';

@Directive()
export class TestDir implements OnDestroy {
@Output() someChange = new EventEmitter<string>();

ngOnDestroy() {
this.someChange.complete();
}
export class TestDir {
@Output() someChange = new EventEmitter();
}

let instance: TestDir;

instance.someChange.pipe();
`);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import {
getUniqueIdForProperty,
isTargetOutputDeclaration,
extractSourceOutputDefinition,
isPotentialProblematicEventEmitterUsage,
isPotentialCompleteCallUsage,
isPotentialNextCallUsage,
isPotentialPipeCallUsage,
} from './output_helpers';
import {
calculateImportReplacements,
calculateDeclarationReplacements,
calculateDeclarationReplacement,
calculateNextFnReplacement,
calculateCompleteCallReplacement,
} from './output-replacements';

interface OutputMigrationData {
Expand Down Expand Up @@ -73,11 +75,11 @@ export class OutputMigration extends TsurgeFunnelMigration<
const relativePath = projectRelativePath(node.getSourceFile(), projectDirAbsPath);

filesWithOutputDeclarations.add(relativePath);
addOutputReplacements(
addOutputReplacement(
outputFieldReplacements,
outputDef.id,
relativePath,
calculateDeclarationReplacements(projectDirAbsPath, node, outputDef.aliasParam),
calculateDeclarationReplacement(projectDirAbsPath, node, outputDef.aliasParam),
);
}
}
Expand All @@ -93,16 +95,43 @@ export class OutputMigration extends TsurgeFunnelMigration<
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(projectDirAbsPath, propertyDeclaration);
const relativePath = projectRelativePath(node.getSourceFile(), projectDirAbsPath);
addOutputReplacements(outputFieldReplacements, id, relativePath, [
addOutputReplacement(
outputFieldReplacements,
id,
relativePath,
calculateNextFnReplacement(projectDirAbsPath, node.expression.name),
]);
);
}
}

// detect .complete usages that should be removed
if (isPotentialCompleteCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(
node.expression.expression,
checker,
reflector,
dtsReader,
);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(projectDirAbsPath, propertyDeclaration);
const relativePath = projectRelativePath(node.getSourceFile(), projectDirAbsPath);
if (ts.isExpressionStatement(node.parent)) {
addOutputReplacement(
outputFieldReplacements,
id,
relativePath,
calculateCompleteCallReplacement(projectDirAbsPath, node.parent),
);
} else {
problematicUsages[id] = true;
}
}
}

// detect unsafe access of the output property
if (isPotentialProblematicEventEmitterUsage(node)) {
if (isPotentialPipeCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(
node.expression,
node.expression.expression,
checker,
reflector,
dtsReader,
Expand Down Expand Up @@ -202,19 +231,19 @@ export class OutputMigration extends TsurgeFunnelMigration<
}
}

function addOutputReplacements(
function addOutputReplacement(
outputFieldReplacements: Record<OutputID, OutputMigrationData>,
outputId: OutputID,
relativePath: ProjectRelativePath,
replacements: Replacement[],
replacement: Replacement,
): void {
const existingReplacements = outputFieldReplacements[outputId];
if (existingReplacements !== undefined) {
existingReplacements.replacements.push(...replacements);
existingReplacements.replacements.push(replacement);
} else {
outputFieldReplacements[outputId] = {
path: relativePath,
replacements: replacements,
replacements: [replacement],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import

const printer = ts.createPrinter();

export function calculateDeclarationReplacements(
export function calculateDeclarationReplacement(
projectDirAbsPath: AbsoluteFsPath,
node: ts.PropertyDeclaration,
aliasParam?: ts.Expression,
): Replacement[] {
): Replacement {
const sf = node.getSourceFile();
const payloadTypes =
node.initializer !== undefined && ts.isNewExpression(node.initializer)
Expand Down Expand Up @@ -53,16 +53,11 @@ export function calculateDeclarationReplacements(
outputCall,
);

return [
new Replacement(
projectRelativePath(sf, projectDirAbsPath),
new TextUpdate({
position: node.getStart(),
end: node.getEnd(),
toInsert: printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf),
}),
),
];
return prepareTextReplacement(
projectDirAbsPath,
node,
printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf),
);
}

export function calculateImportReplacements(
Expand Down Expand Up @@ -103,14 +98,29 @@ export function calculateImportReplacements(
export function calculateNextFnReplacement(
projectDirAbsPath: AbsoluteFsPath,
node: ts.MemberName,
): Replacement {
return prepareTextReplacement(projectDirAbsPath, node, 'emit');
}

export function calculateCompleteCallReplacement(
projectDirAbsPath: AbsoluteFsPath,
node: ts.ExpressionStatement,
): Replacement {
return prepareTextReplacement(projectDirAbsPath, node, '');
}

function prepareTextReplacement(
projectDirAbsPath: AbsoluteFsPath,
node: ts.Node,
replacement: string,
): Replacement {
const sf = node.getSourceFile();
return new Replacement(
projectRelativePath(sf, projectDirAbsPath),
new TextUpdate({
position: node.getStart(),
end: node.getEnd(),
toInsert: 'emit',
toInsert: replacement,
}),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ export interface ExtractedOutput {
aliasParam?: ts.Expression;
}

const PROBLEMATIC_OUTPUT_USAGES = new Set(['complete', 'pipe']);

/**
* Determines if the given node refers to a decorator-based output, and
* returns its resolved metadata if possible.
Expand Down Expand Up @@ -60,29 +58,28 @@ function isOutputDeclarationEligibleForMigration(node: ts.PropertyDeclaration) {
);
}

export function isPotentialProblematicEventEmitterUsage(
node: ts.Node,
): node is ts.PropertyAccessExpression {
return (
ts.isPropertyAccessExpression(node) &&
ts.isIdentifier(node.name) &&
PROBLEMATIC_OUTPUT_USAGES.has(node.name.text)
);
}

export function isPotentialNextCallUsage(node: ts.Node): node is ts.CallExpression {
function isPotentialOutputCallUsage(node: ts.Node, name: string): node is ts.CallExpression {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.name)
) {
const methodName = node.expression.name.text;
if (methodName === 'next') {
return true;
}
return node.expression?.name.text === name;
} else {
return false;
}
}

export function isPotentialPipeCallUsage(node: ts.Node): node is ts.CallExpression {
return isPotentialOutputCallUsage(node, 'pipe');
}

export function isPotentialNextCallUsage(node: ts.Node): node is ts.CallExpression {
return isPotentialOutputCallUsage(node, 'next');
}

return false;
export function isPotentialCompleteCallUsage(node: ts.Node): node is ts.CallExpression {
return isPotentialOutputCallUsage(node, 'complete');
}

export function isTargetOutputDeclaration(
Expand Down