Skip to content

Commit

Permalink
fix(migrations): Add support for removing imports post migration (#52763
Browse files Browse the repository at this point in the history
)

This update removes imports from component decorators and at the top of the files. It only removes standalone imports though. It does not remove CommonModule if that is the only import.

PR Close #52763
  • Loading branch information
jessicajaniuk committed Nov 13, 2023
1 parent 54a7b33 commit aa2d815
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 60 deletions.
Expand Up @@ -82,11 +82,11 @@ function runControlFlowMigration(
const content = tree.readText(relativePath);
const update = tree.beginUpdate(relativePath);

for (const [start, end] of ranges) {
for (const {start, end, node, type} of ranges) {
const template = content.slice(start, end);
const length = (end ?? content.length) - start;

const {migrated, errors} = migrateTemplate(template);
const {migrated, errors} = migrateTemplate(template, type, node, file);

if (migrated !== null) {
update.remove(start, length);
Expand Down
Expand Up @@ -6,26 +6,37 @@
* found in the LICENSE file at https://angular.io/license
*/

import ts from 'typescript';

import {migrateFor} from './fors';
import {migrateIf} from './ifs';
import {migrateSwitch} from './switches';
import {MigrateError} from './types';
import {processNgTemplates} from './util';
import {AnalyzedFile, MigrateError} from './types';
import {canRemoveCommonModule, processNgTemplates, removeImports} from './util';

/**
* Actually migrates a given template to the new syntax
*/
export function migrateTemplate(template: string): {migrated: string, errors: MigrateError[]} {
const ifResult = migrateIf(template);
const forResult = migrateFor(ifResult.migrated);
const switchResult = migrateSwitch(forResult.migrated);
export function migrateTemplate(
template: string, templateType: string, node: ts.Node,
file: AnalyzedFile): {migrated: string, errors: MigrateError[]} {
let errors: MigrateError[] = [];
let migrated = template;
if (templateType === 'template') {
const ifResult = migrateIf(template);
const forResult = migrateFor(ifResult.migrated);
const switchResult = migrateSwitch(forResult.migrated);
migrated = processNgTemplates(switchResult.migrated);
file.removeCommonModule = canRemoveCommonModule(template);

const migrated = processNgTemplates(switchResult.migrated);
errors = [
...ifResult.errors,
...forResult.errors,
...switchResult.errors,
];
} else {
migrated = removeImports(template, node, file.removeCommonModule);
}

const errors = [
...ifResult.errors,
...forResult.errors,
...switchResult.errors,
];
return {migrated, errors};
}
Expand Up @@ -6,15 +6,59 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Attribute, Element, RecursiveVisitor} from '@angular/compiler';
import {Attribute, Element, RecursiveVisitor, Text} from '@angular/compiler';
import ts from 'typescript';

export const ngtemplate = 'ng-template';

function allFormsOf(selector: string): string[] {
return [
selector,
`*${selector}`,
`[${selector}]`,
];
}

const commonModuleDirectives = new Set([
...allFormsOf('ngComponentOutlet'),
...allFormsOf('ngTemplateOutlet'),
...allFormsOf('ngClass'),
...allFormsOf('ngPlural'),
...allFormsOf('ngPluralCase'),
...allFormsOf('ngStyle'),
...allFormsOf('ngTemplateOutlet'),
...allFormsOf('ngComponentOutlet'),
]);

function pipeMatchRegExpFor(name: string): RegExp {
return new RegExp(`\\|\\s*${name}`);
}

const commonModulePipes = [
'date',
'async',
'currency',
'number',
'i18nPlural',
'i18nSelect',
'json',
'keyvalue',
'slice',
'lowercase',
'uppercase',
'titlecase',
'percent',
'titlecase',
].map(name => pipeMatchRegExpFor(name));

/**
* Represents a range of text within a file. Omitting the end
* means that it's until the end of the file.
*/
type Range = [start: number, end?: number];
type Range = {
start: number,
end?: number, node: ts.Node, type: string,
};

export type Offsets = {
pre: number,
Expand Down Expand Up @@ -104,10 +148,17 @@ export class Template {
/** Represents a file that was analyzed by the migration. */
export class AnalyzedFile {
private ranges: Range[] = [];
removeCommonModule = false;

/** Returns the ranges in the order in which they should be migrated. */
getSortedRanges(): Range[] {
return this.ranges.slice().sort(([aStart], [bStart]) => bStart - aStart);
const templateRanges = this.ranges.slice()
.filter(x => x.type === 'template')
.sort((aStart, bStart) => bStart.start - aStart.start);
const importRanges = this.ranges.slice()
.filter(x => x.type === 'import')
.sort((aStart, bStart) => bStart.start - aStart.start);
return [...templateRanges, ...importRanges];
}

/**
Expand All @@ -125,14 +176,44 @@ export class AnalyzedFile {
}

const duplicate =
analysis.ranges.find(current => current[0] === range[0] && current[1] === range[1]);
analysis.ranges.find(current => current.start === range.start && current.end === range.end);

if (!duplicate) {
analysis.ranges.push(range);
}
}
}

/** Finds all non-control flow elements from common module. */
export class CommonCollector extends RecursiveVisitor {
count = 0;

override visitElement(el: Element): void {
if (el.attrs.length > 0) {
for (const attr of el.attrs) {
if (this.hasDirectives(attr.name) || this.hasPipes(attr.value)) {
this.count++;
}
}
}
super.visitElement(el, null);
}

override visitText(ast: Text) {
if (this.hasPipes(ast.value)) {
this.count++;
}
}

private hasDirectives(input: string): boolean {
return commonModuleDirectives.has(input);
}

private hasPipes(input: string): boolean {
return commonModulePipes.some(regexp => regexp.test(input));
}
}

/** Finds all elements with ngif structural directives. */
export class ElementCollector extends RecursiveVisitor {
readonly elements: ElementToMigrate[] = [];
Expand Down

0 comments on commit aa2d815

Please sign in to comment.