Skip to content

Commit b70a409

Browse files
authored
refactor: ensure paths are safely passed around in ng-update (#19354)
1 parent 2635f0c commit b70a409

22 files changed

+205
-95
lines changed

src/cdk/schematics/ng-update/devkit-file-system.ts

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,45 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {normalize} from '@angular-devkit/core';
9+
import {normalize, Path, relative} from '@angular-devkit/core';
1010
import {Tree, UpdateRecorder} from '@angular-devkit/schematics';
11-
import {relative} from 'path';
11+
import * as path from 'path';
1212
import {FileSystem} from '../update-tool/file-system';
1313

1414
/**
1515
* File system that leverages the virtual tree from the CLI devkit. This file
1616
* system is commonly used by `ng update` migrations that run as part of the
1717
* Angular CLI.
1818
*/
19-
export class DevkitFileSystem implements FileSystem {
19+
export class DevkitFileSystem extends FileSystem<Path> {
2020
private _updateRecorderCache = new Map<string, UpdateRecorder>();
21+
private _workspaceFsPath: Path;
2122

22-
constructor(private _tree: Tree, private _workspaceFsPath: string) {}
23+
constructor(private _tree: Tree, workspaceFsPath: string) {
24+
super();
25+
this._workspaceFsPath = normalize(workspaceFsPath);
26+
}
2327

24-
resolve(fsFilePath: string) {
25-
return normalize(relative(this._workspaceFsPath, fsFilePath)) as string;
28+
resolve(...segments: string[]): Path {
29+
// Note: We use `posix.resolve` as the devkit paths are using posix separators.
30+
const resolvedPath = normalize(path.posix.resolve(...segments.map(normalize)));
31+
// If the resolved path points to the workspace root, then this is an absolute disk
32+
// path and we need to compute a devkit tree relative path.
33+
if (resolvedPath.startsWith(this._workspaceFsPath)) {
34+
return relative(this._workspaceFsPath, resolvedPath);
35+
}
36+
// Otherwise we know that the path is absolute (due to the resolve), and that it
37+
// refers to an absolute devkit tree path (like `/angular.json`). We keep those
38+
// unmodified as they are already resolved workspace paths.
39+
return resolvedPath;
2640
}
2741

28-
edit(fsFilePath: string) {
29-
const treeFilePath = this.resolve(fsFilePath);
30-
if (this._updateRecorderCache.has(treeFilePath)) {
31-
return this._updateRecorderCache.get(treeFilePath)!;
42+
edit(filePath: Path) {
43+
if (this._updateRecorderCache.has(filePath)) {
44+
return this._updateRecorderCache.get(filePath)!;
3245
}
33-
const recorder = this._tree.beginUpdate(treeFilePath);
34-
this._updateRecorderCache.set(treeFilePath, recorder);
46+
const recorder = this._tree.beginUpdate(filePath);
47+
this._updateRecorderCache.set(filePath, recorder);
3548
return recorder;
3649
}
3750

@@ -40,24 +53,24 @@ export class DevkitFileSystem implements FileSystem {
4053
this._updateRecorderCache.clear();
4154
}
4255

43-
exists(fsFilePath: string) {
44-
return this._tree.exists(this.resolve(fsFilePath));
56+
exists(filePath: Path) {
57+
return this._tree.exists(filePath);
4558
}
4659

47-
overwrite(fsFilePath: string, content: string) {
48-
this._tree.overwrite(this.resolve(fsFilePath), content);
60+
overwrite(filePath: Path, content: string) {
61+
this._tree.overwrite(filePath, content);
4962
}
5063

51-
create(fsFilePath: string, content: string) {
52-
this._tree.create(this.resolve(fsFilePath), content);
64+
create(filePath: Path, content: string) {
65+
this._tree.create(filePath, content);
5366
}
5467

55-
delete(fsFilePath: string) {
56-
this._tree.delete(this.resolve(fsFilePath));
68+
delete(filePath: Path) {
69+
this._tree.delete(filePath);
5770
}
5871

59-
read(fsFilePath: string) {
60-
const buffer = this._tree.read(this.resolve(fsFilePath));
72+
read(filePath: Path) {
73+
const buffer = this._tree.read(filePath);
6174
return buffer !== null ? buffer.toString() : null;
6275
}
6376
}

src/cdk/schematics/ng-update/devkit-migration-rule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {join} from 'path';
1515
import {UpdateProject} from '../update-tool';
1616
import {MigrationCtor} from '../update-tool/migration';
1717
import {TargetVersion} from '../update-tool/target-version';
18+
import {WorkspacePath} from '../update-tool/file-system';
1819
import {getTargetTsconfigPath, getWorkspaceConfigGracefully} from '../utils/project-tsconfig-paths';
1920

2021
import {DevkitFileSystem} from './devkit-file-system';
@@ -72,7 +73,7 @@ export function createMigrationSchematicRule(
7273
// Keep track of all project source files which have been checked/migrated. This is
7374
// necessary because multiple TypeScript projects can contain the same source file and
7475
// we don't want to check these again, as this would result in duplicated failure messages.
75-
const analyzedFiles = new Set<string>();
76+
const analyzedFiles = new Set<WorkspacePath>();
7677
// The CLI uses the working directory as the base directory for the virtual file system tree.
7778
const workspaceFsPath = process.cwd();
7879
const fileSystem = new DevkitFileSystem(tree, workspaceFsPath);

src/cdk/schematics/ng-update/migrations/attribute-selectors.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import * as ts from 'typescript';
10+
import {WorkspacePath} from '../../update-tool/file-system';
1011
import {ResolvedResource} from '../../update-tool/component-resource-collector';
1112
import {Migration} from '../../update-tool/migration';
1213
import {AttributeSelectorUpgradeData} from '../data/attribute-selectors';
@@ -63,11 +64,13 @@ export class AttributeSelectorsMigration extends Migration<UpgradeData> {
6364
this.data.forEach(selector => {
6465
findAllSubstringIndices(literalText, selector.replace)
6566
.map(offset => literal.getStart() + offset)
66-
.forEach(start => this._replaceSelector(filePath, start, selector));
67+
.forEach(start => this._replaceSelector(
68+
this.fileSystem.resolve(filePath), start, selector));
6769
});
6870
}
6971

70-
private _replaceSelector(filePath: string, start: number, data: AttributeSelectorUpgradeData) {
72+
private _replaceSelector(filePath: WorkspacePath, start: number,
73+
data: AttributeSelectorUpgradeData) {
7174
this.fileSystem.edit(filePath)
7275
.remove(start, data.replace.length)
7376
.insertRight(start, data.replaceWith);

src/cdk/schematics/ng-update/migrations/class-names.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ export class ClassNamesMigration extends Migration<UpgradeData> {
9797
/** Creates a failure and replacement for the specified identifier. */
9898
private _createFailureWithReplacement(identifier: ts.Identifier) {
9999
const classData = this.data.find(data => data.replace === identifier.text)!;
100+
const filePath = this.fileSystem.resolve(identifier.getSourceFile().fileName);
100101

101-
this.fileSystem.edit(identifier.getSourceFile().fileName)
102+
this.fileSystem.edit(filePath)
102103
.remove(identifier.getStart(), identifier.getWidth())
103104
.insertRight(identifier.getStart(), classData.replaceWith);
104105
}

src/cdk/schematics/ng-update/migrations/css-selectors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import * as ts from 'typescript';
10+
import {WorkspacePath} from '../../update-tool/file-system';
1011
import {ResolvedResource} from '../../update-tool/component-resource-collector';
1112
import {Migration} from '../../update-tool/migration';
1213
import {CssSelectorUpgradeData} from '../data/css-selectors';
@@ -69,11 +70,11 @@ export class CssSelectorsMigration extends Migration<UpgradeData> {
6970

7071
findAllSubstringIndices(textContent, data.replace)
7172
.map(offset => node.getStart() + offset)
72-
.forEach(start => this._replaceSelector(filePath, start, data));
73+
.forEach(start => this._replaceSelector(this.fileSystem.resolve(filePath), start, data));
7374
});
7475
}
7576

76-
private _replaceSelector(filePath: string, start: number, data: CssSelectorUpgradeData) {
77+
private _replaceSelector(filePath: WorkspacePath, start: number, data: CssSelectorUpgradeData) {
7778
this.fileSystem.edit(filePath)
7879
.remove(start, data.replace.length)
7980
.insertRight(start, data.replaceWith);

src/cdk/schematics/ng-update/migrations/element-selectors.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import * as ts from 'typescript';
1010
import {ResolvedResource} from '../../update-tool/component-resource-collector';
11+
import {WorkspacePath} from '../../update-tool/file-system';
1112
import {Migration} from '../../update-tool/migration';
1213
import {ElementSelectorUpgradeData} from '../data/element-selectors';
1314
import {findAllSubstringIndices} from '../typescript/literal';
@@ -57,11 +58,13 @@ export class ElementSelectorsMigration extends Migration<UpgradeData> {
5758
this.data.forEach(selector => {
5859
findAllSubstringIndices(textContent, selector.replace)
5960
.map(offset => node.getStart() + offset)
60-
.forEach(start => this._replaceSelector(filePath, start, selector));
61+
.forEach(start => this._replaceSelector(
62+
this.fileSystem.resolve(filePath), start, selector));
6163
});
6264
}
6365

64-
private _replaceSelector(filePath: string, start: number, data: ElementSelectorUpgradeData) {
66+
private _replaceSelector(filePath: WorkspacePath, start: number,
67+
data: ElementSelectorUpgradeData) {
6568
this.fileSystem.edit(filePath)
6669
.remove(start, data.replace.length)
6770
.insertRight(start, data.replaceWith);

src/cdk/schematics/ng-update/migrations/input-names.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {WorkspacePath} from '../../update-tool/file-system';
910
import {findInputsOnElementWithAttr, findInputsOnElementWithTag} from '../html-parsing/angular';
1011
import {ResolvedResource} from '../../update-tool/component-resource-collector';
1112
import {Migration} from '../../update-tool/migration';
@@ -63,7 +64,8 @@ export class InputNamesMigration extends Migration<UpgradeData> {
6364
});
6465
}
6566

66-
private _replaceInputName(filePath: string, start: number, width: number, newName: string) {
67+
private _replaceInputName(filePath: WorkspacePath, start: number, width: number,
68+
newName: string) {
6769
this.fileSystem.edit(filePath)
6870
.remove(start, width)
6971
.insertRight(start, newName);

src/cdk/schematics/ng-update/migrations/output-names.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {WorkspacePath} from '../../update-tool/file-system';
910
import {ResolvedResource} from '../../update-tool/component-resource-collector';
1011
import {Migration} from '../../update-tool/migration';
1112

@@ -49,7 +50,8 @@ export class OutputNamesMigration extends Migration<UpgradeData> {
4950
});
5051
}
5152

52-
private _replaceOutputName(filePath: string, start: number, width: number, newName: string) {
53+
private _replaceOutputName(filePath: WorkspacePath, start: number, width: number,
54+
newName: string) {
5355
this.fileSystem.edit(filePath)
5456
.remove(start, width)
5557
.insertRight(start, newName);

src/cdk/schematics/ng-update/migrations/property-names.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class PropertyNamesMigration extends Migration<UpgradeData> {
3939
}
4040

4141
if (!data.whitelist || data.whitelist.classes.includes(typeName)) {
42-
this.fileSystem.edit(node.getSourceFile().fileName)
42+
this.fileSystem.edit(this.fileSystem.resolve(node.getSourceFile().fileName))
4343
.remove(node.name.getStart(), node.name.getWidth())
4444
.insertRight(node.name.getStart(), data.replaceWith);
4545
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {resolveBazelPath} from '@angular/cdk/schematics/testing';
2+
import {MIGRATION_PATH} from '../../../index.spec';
3+
import {createTestCaseSetup} from '../../../testing';
4+
5+
describe('ng-update external resource resolution', () => {
6+
7+
it('should properly resolve referenced resources in components', async () => {
8+
const {runFixers, writeFile, removeTempDir, appTree} = await createTestCaseSetup(
9+
'migration-v6', MIGRATION_PATH,
10+
[resolveBazelPath(__dirname, './external-resource-resolution_input.ts')]);
11+
12+
const testContent = `<div cdk-connected-overlay [origin]="test"></div>`;
13+
const expected = `<div cdk-connected-overlay [cdkConnectedOverlayOrigin]="test"></div>`;
14+
15+
writeFile('/projects/material/test.html', testContent);
16+
writeFile('/projects/cdk-testing/src/some-tmpl.html', testContent);
17+
writeFile('/projects/cdk-testing/src/test-cases/local.html', testContent);
18+
19+
await runFixers();
20+
21+
expect(appTree.readContent('/projects/material/test.html'))
22+
.toBe(expected, 'Expected absolute devkit tree paths to work.');
23+
expect(appTree.readContent('/projects/cdk-testing/src/some-tmpl.html'))
24+
.toBe(expected, 'Expected relative paths with parent segments to work.');
25+
expect(appTree.readContent('/projects/cdk-testing/src/test-cases/local.html'))
26+
.toBe(expected, 'Expected relative paths without explicit dot to work.');
27+
28+
removeTempDir();
29+
});
30+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
selector: 'test-cmp',
5+
templateUrl: '/projects/material/test.html',
6+
})
7+
export class MyTestComp {}
8+
9+
@Component({
10+
selector: 'test-cmp2',
11+
templateUrl: '../some-tmpl.html',
12+
})
13+
export class MyTestComp2 {}
14+
15+
@Component({
16+
selector: 'test-cmp3',
17+
templateUrl: 'local.html',
18+
})
19+
export class MyTestComp3 {}

src/cdk/schematics/testing/test-case-setup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {getSystemPath, normalize} from '@angular-devkit/core';
9+
import {getSystemPath, normalize, Path} from '@angular-devkit/core';
1010
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
1111
import * as virtualFs from '@angular-devkit/core/src/virtual-fs/host';
1212
import {HostTree, Tree} from '@angular-devkit/schematics';
@@ -224,13 +224,13 @@ export function defineJasmineTestCases(versionName: string, collectionFile: stri
224224
*/
225225
export function _patchTypeScriptDefaultLib(tree: Tree) {
226226
const _originalRead = tree.read;
227-
tree.read = function(filePath: string) {
227+
tree.read = function(filePath: Path) {
228228
// In case a file within the TypeScript package is requested, we read the file from
229229
// the real file system. This is necessary because within unit tests, the "typeScript"
230230
// package from within the Bazel "@npm" repository is used. The virtual tree can't be
231231
// used because the "@npm" repository directory is not part of the virtual file system.
232232
if (filePath.match(/node_modules[/\\]typescript/)) {
233-
return readFileSync(filePath);
233+
return readFileSync(getSystemPath(filePath));
234234
} else {
235235
return _originalRead.apply(this, arguments);
236236
}

src/cdk/schematics/update-tool/component-resource-collector.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {dirname, resolve} from 'path';
9+
import {dirname} from 'path';
1010
import * as ts from 'typescript';
11-
import {FileSystem} from './file-system';
11+
import {FileSystem, WorkspacePath} from './file-system';
1212
import {getAngularDecorators} from './utils/decorators';
1313
import {unwrapExpression} from './utils/functions';
1414
import {
@@ -28,7 +28,7 @@ export interface ResolvedResource {
2828
/** Whether the given resource is inline or not. */
2929
inline: boolean;
3030
/** Path to the file that contains this resource. */
31-
filePath: string;
31+
filePath: WorkspacePath;
3232
/**
3333
* Gets the character and line of a given position index in the resource.
3434
* If the resource is declared inline within a TypeScript source file, the line and
@@ -81,7 +81,8 @@ export class ComponentResourceCollector {
8181
}
8282

8383
const sourceFile = node.getSourceFile();
84-
const sourceFileName = sourceFile.fileName;
84+
const filePath = this._fileSystem.resolve(sourceFile.fileName);
85+
const sourceFileDirPath = dirname(sourceFile.fileName);
8586

8687
// Walk through all component metadata properties and determine the referenced
8788
// HTML templates (either external or inline)
@@ -91,7 +92,6 @@ export class ComponentResourceCollector {
9192
}
9293

9394
const propertyName = getPropertyNameText(property.name);
94-
const filePath = resolve(sourceFileName);
9595

9696
if (propertyName === 'styles' && ts.isArrayLiteralExpression(property.initializer)) {
9797
property.initializer.elements.forEach(el => {
@@ -100,7 +100,7 @@ export class ComponentResourceCollector {
100100
// not part of the template content.
101101
const templateStartIdx = el.getStart() + 1;
102102
this.resolvedStylesheets.push({
103-
filePath: filePath,
103+
filePath,
104104
container: node,
105105
content: el.text,
106106
inline: true,
@@ -119,7 +119,7 @@ export class ComponentResourceCollector {
119119
// not part of the template content.
120120
const templateStartIdx = property.initializer.getStart() + 1;
121121
this.resolvedTemplates.push({
122-
filePath: filePath,
122+
filePath,
123123
container: node,
124124
content: property.initializer.text,
125125
inline: true,
@@ -132,7 +132,7 @@ export class ComponentResourceCollector {
132132
if (propertyName === 'styleUrls' && ts.isArrayLiteralExpression(property.initializer)) {
133133
property.initializer.elements.forEach(el => {
134134
if (ts.isStringLiteralLike(el)) {
135-
const stylesheetPath = resolve(dirname(sourceFileName), el.text);
135+
const stylesheetPath = this._fileSystem.resolve(sourceFileDirPath, el.text);
136136

137137
// In case the stylesheet does not exist in the file system, skip it gracefully.
138138
if (!this._fileSystem.exists(stylesheetPath)) {
@@ -145,7 +145,8 @@ export class ComponentResourceCollector {
145145
}
146146

147147
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
148-
const templatePath = resolve(dirname(sourceFileName), property.initializer.text);
148+
const templateUrl = property.initializer.text;
149+
const templatePath = this._fileSystem.resolve(sourceFileDirPath, templateUrl);
149150

150151
// In case the template does not exist in the file system, skip this
151152
// external template.
@@ -169,7 +170,7 @@ export class ComponentResourceCollector {
169170
}
170171

171172
/** Resolves an external stylesheet by reading its content and computing line mappings. */
172-
resolveExternalStylesheet(filePath: string, container: ts.ClassDeclaration|null):
173+
resolveExternalStylesheet(filePath: WorkspacePath, container: ts.ClassDeclaration|null):
173174
ResolvedResource {
174175
const fileContent = this._fileSystem.read(filePath);
175176
const lineStartsMap = computeLineStartsMap(fileContent);

0 commit comments

Comments
 (0)