Skip to content
Open
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 @@ -68,6 +68,8 @@ export interface MigrationConfig {
shouldMigrate?: (containingFile: ProjectFile) => boolean;
}

const SAFE_NAVIGATION_MIGRATION_FN = '$safeNavigationMigration';

/**
* This migration wraps optional chaining expressions in Angular templates with a call to the
* `$safeNavigationMigration()` magic function. This function doesn't exist at runtime, but is
Expand Down Expand Up @@ -371,6 +373,11 @@ class ExpressionMigrator extends RecursiveAstVisitor {
// ---------------------------------------------------------------------------

override visitCall(ast: Call, nullSensitive: boolean): any {
if (isSafeNavigationMigrationCall(ast)) {
Comment thread
atscott marked this conversation as resolved.
this.visit(ast.receiver, false);
return;
}

if (nullSensitive && this.hasSafeReceiver(ast.receiver)) {
this.addReplacement(ast);
}
Expand Down Expand Up @@ -503,6 +510,16 @@ function isNullishLiteralAST(ast: AST): boolean {
);
}

function isSafeNavigationMigrationCall(ast: AST): boolean {
const innerAst = ast instanceof ASTWithSource ? ast.ast : ast;

return (
innerAst instanceof Call &&
innerAst.receiver instanceof PropertyRead &&
innerAst.receiver.name === SAFE_NAVIGATION_MIGRATION_FN
);
}

/** Returns true if the AST node is a non-null, non-undefined primitive literal. */
function isNonNullishLiteralAST(ast: AST): boolean {
const innerAst = ast instanceof ASTWithSource ? ast.ast : ast;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,27 +515,27 @@ describe('SafeOptionalChainingMigration', () => {
{{ foo?.bar | json }}
<div [id]="user?.id"></div>
<div *ngIf="foo?.bar === null"></div>
{{ compute(my?.utility?.service(my?.optional?.argument)) }}
`;

// First pass: migrate fresh code
const firstPass = await migrateInlineTemplate(input);
const firstPass = await migrateInlineTemplateContent(input);

// Verify all expressions are wrapped correctly on first pass
expect(firstPass).toContain('{{ compute($safeNavigationMigration(foo?.bar)) }}');
expect(firstPass).toContain('{{ $safeNavigationMigration(foo?.bar) | json }}');
expect(firstPass).toContain('<div [id]="$safeNavigationMigration(user?.id)"></div>');
expect(firstPass).toContain('<div *ngIf="$safeNavigationMigration(foo?.bar) === null"></div>');
expect(firstPass).toContain(
'{{ compute($safeNavigationMigration(my?.utility?.service($safeNavigationMigration(my?.optional?.argument)))) }}',
);

// Second pass: run migration again on already-migrated code
const secondPass = await migrateInlineTemplate(firstPass);
const secondPass = await migrateInlineTemplateContent(firstPass);

// Verify no double-wrapping occurred
expect(secondPass).not.toContain('$safeNavigationMigration($safeNavigationMigration');
// The already-wrapped expressions should remain unchanged
expect(secondPass).toContain('{{ compute($safeNavigationMigration(foo?.bar)) }}');
expect(secondPass).toContain('{{ $safeNavigationMigration(foo?.bar) | json }}');
expect(secondPass).toContain('<div [id]="$safeNavigationMigration(user?.id)"></div>');
expect(secondPass).toContain('<div *ngIf="$safeNavigationMigration(foo?.bar) === null"></div>');
expect(normalizeTemplateContent(secondPass)).toBe(normalizeTemplateContent(firstPass));
});
});

Expand All @@ -552,13 +552,28 @@ async function migrateInlineTemplate(template: string): Promise<string> {
${template}
\`
})
export class AppComponent { foo: any; compute(a: any) {} }
export class AppComponent { foo: any; compute(a: any) {}; my:any; }
`,
},
]);
return fs.readFile(absoluteFrom('/app.component.ts'));
}

async function migrateInlineTemplateContent(template: string): Promise<string> {
const contents = await migrateInlineTemplate(template);
const match = contents.match(/template:\s*`([\s\S]*?)`\s*}\)\s*export class/);

if (match === null) {
throw new Error('Failed to extract migrated inline template content.');
}

return match[1];
}

function normalizeTemplateContent(template: string): string {
return template.replace(/^\s+|\s+$/g, '');
}

async function migrateExternalTemplate(template: string): Promise<string> {
const {fs} = await runTsurgeMigration(new SafeOptionalChainingMigration(), [
{
Expand Down
Loading