Skip to content

Commit 5e1eae6

Browse files
authored
Merge pull request #6605 from IgniteUI/dpetev/identifier-migrations
refactor(migrations): limit identifier rename from package imports
2 parents 1c6bffa + 19bcc5c commit 5e1eae6

File tree

4 files changed

+209
-16
lines changed

4 files changed

+209
-16
lines changed

.vscode/launch.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
"ts-node/register",
1414
"./node_modules/jasmine/bin/jasmine.js",
1515
"./projects/igniteui-angular/migrations/common/UpdateChanges.spec.ts"
16-
]
16+
],
17+
"env": {
18+
"TS_NODE_PROJECT": "projects/igniteui-angular/migrations/tsconfig.json"
19+
}
1720
}
1821
]
1922
}

projects/igniteui-angular/migrations/common/UpdateChanges.spec.ts

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as fs from 'fs';
66
import * as path from 'path';
77
import { ClassChanges, BindingChanges, SelectorChanges, ThemePropertyChanges, ImportsChanges } from './schema';
88
import { UpdateChanges } from './UpdateChanges';
9+
import * as tsUtils from './tsUtils';
910

1011
describe('UpdateChanges', () => {
1112
let appTree: UnitTestTree;
@@ -226,7 +227,8 @@ describe('UpdateChanges', () => {
226227
});
227228
spyOn<any>(fs, 'readFileSync').and.callFake(() => JSON.stringify(classJson));
228229

229-
const fileContent = `import { igxClass } from ""; export class Test { prop: igxClass; prop2: igxClass2; }`;
230+
const fileContent =
231+
`import { igxClass, igxClass2 } from "igniteui-angular"; export class Test { prop: igxClass; prop2: igxClass2; }`;
230232
appTree.create('test.component.ts', fileContent);
231233

232234
const update = new UnitUpdateChanges(__dirname, appTree);
@@ -236,7 +238,7 @@ describe('UpdateChanges', () => {
236238

237239
update.applyChanges();
238240
expect(appTree.readContent('test.component.ts')).toEqual(
239-
`import { igxReplace } from ""; export class Test { prop: igxReplace; prop2: igxSecond; }`);
241+
`import { igxReplace, igxSecond } from "igniteui-angular"; export class Test { prop: igxReplace; prop2: igxSecond; }`);
240242

241243
done();
242244
});
@@ -268,13 +270,13 @@ describe('UpdateChanges', () => {
268270

269271
const fileContent =
270272
`import { Component, Injectable, ViewChild } from "@angular/core";` +
271-
`import { IgxGridComponent } from "../../lib/grid/grid.component";` +
272-
`import { IgxColumnComponent, IgxProvided, STRING_FILTERS} from "../../lib/main";\r\n` +
273+
`import { IgxGridComponent } from "igniteui-angular";` +
274+
`import { IgxColumnComponent, IgxProvided, STRING_FILTERS} from "igniteui-angular";\r\n` +
273275
`import {` +
274276
` IgxCsvExporterService,` +
275277
` IgxExcelExporterOptions,` +
276278
` IgxExporterOptionsBase` +
277-
`} from "../../lib/services/index";\r\n` +
279+
`} from "igniteui-angular";\r\n` +
278280
`@Component({` +
279281
` providers: [IgxProvided, RemoteService]` +
280282
`})` +
@@ -306,13 +308,13 @@ describe('UpdateChanges', () => {
306308
update.applyChanges();
307309
expect(appTree.readContent('test.component.ts')).toEqual(
308310
`import { Component, Injectable, ViewChild } from "@angular/core";` +
309-
`import { IgxGridReplace } from "../../lib/grid/grid.component";` +
310-
`import { IgxColumnReplace, IgxProvidedReplace, REPLACED_CONST} from "../../lib/main";\r\n` +
311+
`import { IgxGridReplace } from "igniteui-angular";` +
312+
`import { IgxColumnReplace, IgxProvidedReplace, REPLACED_CONST} from "igniteui-angular";\r\n` +
311313
`import {` +
312314
` Injected,` +
313315
` IgxNewable,` +
314316
` ReturnType` +
315-
`} from "../../lib/services/index";\r\n` +
317+
`} from "igniteui-angular";\r\n` +
316318
`@Component({` +
317319
` providers: [IgxProvidedReplace, RemoteService]` +
318320
`})` +
@@ -338,6 +340,93 @@ describe('UpdateChanges', () => {
338340
done();
339341
});
340342

343+
it('should correctly ignore types not from igniteui-angular', () => {
344+
const classJson: ClassChanges = {
345+
changes: [
346+
{ name: 'Name', replaceWith: 'NameName' },
347+
{ name: 'Another', replaceWith: 'Other' },
348+
]
349+
};
350+
const jsonPath = path.join(__dirname, 'changes', 'classes.json');
351+
spyOn(fs, 'existsSync').and.callFake((filePath: string) => {
352+
if (filePath === jsonPath) {
353+
return true;
354+
}
355+
return false;
356+
});
357+
spyOn<any>(fs, 'readFileSync').and.callFake(() => JSON.stringify(classJson));
358+
359+
const fileContent =
360+
`import { Name } from ""; import { Another } from "@space/package"; export class Test { prop: Name; prop2: Another; }`;
361+
appTree.create('test.component.ts', fileContent);
362+
363+
const update = new UnitUpdateChanges(__dirname, appTree);
364+
expect(update.getClassChanges()).toEqual(classJson);
365+
366+
spyOn(tsUtils, 'getRenamePositions').and.callThrough();
367+
368+
update.applyChanges();
369+
expect(tsUtils.getRenamePositions).toHaveBeenCalledWith('/test.component.ts', 'Name', jasmine.anything());
370+
expect(tsUtils.getRenamePositions).toHaveBeenCalledWith('/test.component.ts', 'Another', jasmine.anything());
371+
expect(appTree.readContent('test.component.ts')).toEqual(fileContent);
372+
});
373+
374+
it('should correctly rename aliased imports and handle collision from other packages', () => {
375+
const classJson: ClassChanges = {
376+
changes: [
377+
{ name: 'Type', replaceWith: 'IgxType' },
378+
{ name: 'Size', replaceWith: 'IgxSize' },
379+
{ name: 'IgxService', replaceWith: 'IgxService1' },
380+
{ name: 'IgxDiffService', replaceWith: 'IgxNewDiffService' },
381+
{ name: 'Calendar', replaceWith: 'CalendarActual'}
382+
]
383+
};
384+
const jsonPath = path.join(__dirname, 'changes', 'classes.json');
385+
spyOn(fs, 'existsSync').and.callFake((filePath: string) => {
386+
if (filePath === jsonPath) {
387+
return true;
388+
}
389+
return false;
390+
});
391+
spyOn<any>(fs, 'readFileSync').and.callFake(() => JSON.stringify(classJson));
392+
393+
const fileContent =
394+
`import { Size, Type as someThg } from "igniteui-angular";
395+
import { IgxService, IgxDiffService as eDiffService, Calendar as Calendar } from 'igniteui-angular';
396+
import { Type } from "@angular/core";
397+
export class Test {
398+
prop: Type;
399+
prop1: someThg;
400+
prop2: Size = { prop: "Size" };
401+
secondary: eDiffService;
402+
cal: Calendar;
403+
404+
constructor (public router: Router, private _iconService: IgxService) {}
405+
}`;
406+
appTree.create('test.component.ts', fileContent);
407+
408+
const update = new UnitUpdateChanges(__dirname, appTree);
409+
expect(fs.existsSync).toHaveBeenCalledWith(jsonPath);
410+
expect(fs.readFileSync).toHaveBeenCalledWith(jsonPath, 'utf-8');
411+
expect(update.getClassChanges()).toEqual(classJson);
412+
413+
update.applyChanges();
414+
expect(appTree.readContent('test.component.ts')).toEqual(
415+
`import { IgxSize, IgxType as someThg } from "igniteui-angular";
416+
import { IgxService1, IgxNewDiffService as eDiffService, CalendarActual as Calendar } from 'igniteui-angular';
417+
import { Type } from "@angular/core";
418+
export class Test {
419+
prop: Type;
420+
prop1: someThg;
421+
prop2: IgxSize = { prop: "Size" };
422+
secondary: eDiffService;
423+
cal: Calendar;
424+
425+
constructor (public router: Router, private _iconService: IgxService1) {}
426+
}`
427+
);
428+
});
429+
341430
it('should move property value between element tags', done => {
342431
const inputJson: BindingChanges = {
343432
changes: [

projects/igniteui-angular/migrations/common/UpdateChanges.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { WorkspaceSchema } from '@schematics/angular/utility/workspace-models';
44
import * as fs from 'fs';
55
import * as path from 'path';
66
import { ClassChanges, BindingChanges, SelectorChange, SelectorChanges, ThemePropertyChanges, ImportsChanges } from './schema';
7-
import { getIdentifierPositions } from './tsUtils';
7+
import { getLanguageService, getRenamePositions } from './tsUtils';
88
import { getProjectPaths, getWorkspace, getProjects, escapeRegExp } from './util';
9+
import { LanguageService } from 'typescript';
910

1011
// tslint:disable:arrow-parens
1112
export class UpdateChanges {
@@ -60,6 +61,14 @@ export class UpdateChanges {
6061
return this._sassFiles;
6162
}
6263

64+
private _service: LanguageService;
65+
public get service(): LanguageService {
66+
if (!this._service) {
67+
this._service = getLanguageService(this.tsFiles, this.host);
68+
}
69+
return this._service;
70+
}
71+
6372
/**
6473
* Create a new base schematic to apply changes
6574
* @param rootPath Root folder for the schematic to read configs, pass __dirname
@@ -169,21 +178,17 @@ export class UpdateChanges {
169178

170179
protected updateClasses(entryPath: string) {
171180
let fileContent = this.host.read(entryPath).toString();
172-
let overwrite = false;
173181
for (const change of this.classChanges.changes) {
174182
if (fileContent.indexOf(change.name) !== -1) {
175-
const positions = getIdentifierPositions(fileContent, change.name);
183+
const positions = getRenamePositions(entryPath, change.name, this.service);
176184
// loop backwards to preserve positions
177185
for (let i = positions.length; i--;) {
178186
const pos = positions[i];
179187
fileContent = fileContent.slice(0, pos.start) + change.replaceWith + fileContent.slice(pos.end);
180188
}
181-
overwrite = true;
189+
this.host.overwrite(entryPath, fileContent);
182190
}
183191
}
184-
if (overwrite) {
185-
this.host.overwrite(entryPath, fileContent);
186-
}
187192
}
188193

189194
protected updateBindings(entryPath: string, bindChanges: BindingChanges, type = BindingType.output) {

projects/igniteui-angular/migrations/common/tsUtils.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// tslint:disable-next-line:no-implicit-dependencies
22
import * as ts from 'typescript';
3+
import { Tree } from '@angular-devkit/schematics';
4+
5+
export const PACKAGE_IMPORT = 'igniteui-angular';
36

47
/** Returns an source file */
58
// export function getFileSource(sourceText: string): ts.SourceFile {
@@ -57,3 +60,96 @@ export function getImportModulePositions(sourceText: string, startsWith: string)
5760
}
5861
return positions;
5962
}
63+
64+
/** Filters out statements to named imports (e.g. `import {x, y}`) from PACKAGE_IMPORT */
65+
const namedImportFilter = (statement: ts.Statement) => {
66+
if (statement.kind === ts.SyntaxKind.ImportDeclaration &&
67+
((statement as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text === PACKAGE_IMPORT) {
68+
69+
const clause = (statement as ts.ImportDeclaration).importClause;
70+
return clause && clause.namedBindings && clause.namedBindings.kind === ts.SyntaxKind.NamedImports;
71+
}
72+
return false;
73+
};
74+
75+
export function getRenamePositions(sourcePath: string, name: string, service: ts.LanguageService):
76+
Array<{start: number, end: number}> {
77+
78+
const source = service.getProgram().getSourceFile(sourcePath);
79+
const positions = [];
80+
81+
const imports = source.statements.filter(<(a: ts.Statement) => a is ts.ImportDeclaration>namedImportFilter);
82+
if (!imports.length) {
83+
return positions;
84+
}
85+
const elements: ts.NodeArray<ts.ImportSpecifier> =
86+
imports
87+
.map(x => (x.importClause.namedBindings as ts.NamedImports).elements)
88+
.reduce((prev, current) => prev.concat(current) as unknown as ts.NodeArray<ts.ImportSpecifier>);
89+
90+
for (const elem of elements) {
91+
if (elem.propertyName && elem.propertyName.text === name) {
92+
// alias imports `igxClass as smth` -> <propertyName> as <name>
93+
// other references are only for the name portion
94+
positions.push({ start: elem.propertyName.getStart(), end: elem.propertyName.getEnd() });
95+
break;
96+
}
97+
98+
if (!elem.propertyName && elem.name.text === name) {
99+
const renames = service.findRenameLocations(sourcePath, elem.name.getStart(), false, false, false);
100+
if (renames) {
101+
const renamesPos = renames.map(x => ({ start: x.textSpan.start, end: x.textSpan.start + x.textSpan.length}) );
102+
positions.push(...renamesPos);
103+
}
104+
}
105+
}
106+
107+
return positions;
108+
}
109+
110+
//#region Language Service
111+
112+
/**
113+
* Create a TypeScript language service
114+
* @param filePaths Paths for files to include for the language service host
115+
* @param host Virtual FS host
116+
* @param options Typescript compiler options for the service
117+
*/
118+
export function getLanguageService(filePaths: string[], host: Tree, options: ts.CompilerOptions = {}): ts.LanguageService {
119+
// https://stackoverflow.com/a/14221483
120+
const fileVersions = new Map<string, number>();
121+
patchHostOverwrite(host, fileVersions);
122+
123+
const servicesHost: ts.LanguageServiceHost = {
124+
getCompilationSettings: () => options,
125+
getScriptFileNames: () => filePaths,
126+
getScriptVersion: fileName => {
127+
// return host.actions.filter(x => x.path === fileName && x.kind !== 'c').length.toString();
128+
const version = fileVersions.get(fileName) || 0;
129+
return version.toString();
130+
},
131+
getScriptSnapshot: fileName => {
132+
if (!host.exists(fileName)) {
133+
return undefined;
134+
}
135+
return ts.ScriptSnapshot.fromString(host.read(fileName).toString());
136+
},
137+
getCurrentDirectory: () => process.cwd(),
138+
getDefaultLibFileName: opts => ts.getDefaultLibFilePath(opts),
139+
fileExists: fileName => {
140+
return filePaths.indexOf(fileName) !== -1;
141+
}
142+
};
143+
return ts.createLanguageService(servicesHost, ts.createDocumentRegistry());
144+
}
145+
146+
function patchHostOverwrite(host: Tree, fileVersions: Map<string, number>) {
147+
const original = host.overwrite;
148+
host.overwrite = (path: string, content: Buffer | string) => {
149+
const version = fileVersions.get(path) || 0;
150+
fileVersions.set(path, version + 1);
151+
original.call(host, path, content);
152+
};
153+
}
154+
155+
//#endregion

0 commit comments

Comments
 (0)