Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.

Commit d09c839

Browse files
tomv564felixfbecker
authored andcommitted
Cross-platform path handling cleanup (#260)
* exclude resolve support from other path-uri conversion functions path2uri -> resolvepath2uri new path2uri and uri2path are absolute and detect windows uris/paths without strict or checking process add tests from whatwg url conversion * detect platform from root when resolving to uri * localfilesystem tests should be platform-ignorant * Remove os/strict references from util * Support windows paths in memfs, allow unix path in code fix * Fix resolution by speaking unix paths to typescript module resolver * Use valid uris in project manager tests * Remove old commented out calls * Return content from overlay in getContent * Take overlay into account on fileExists
1 parent 91232a7 commit d09c839

File tree

12 files changed

+118
-128
lines changed

12 files changed

+118
-128
lines changed

src/fs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import iterate from 'iterare';
55
import { Span } from 'opentracing';
66
import Semaphore from 'semaphore-async-await';
77
import { InMemoryFileSystem } from './memfs';
8-
import { normalizeUri, uriToLocalPath } from './util';
8+
import { normalizeUri, uri2path } from './util';
99

1010
export interface FileSystem {
1111
/**
@@ -58,7 +58,7 @@ export class LocalFileSystem implements FileSystem {
5858
* Converts the URI to an absolute path on the local disk
5959
*/
6060
protected resolveUriToPath(uri: string): string {
61-
return uriToLocalPath(uri);
61+
return uri2path(uri);
6262
}
6363

6464
async getWorkspaceFiles(base = this.rootUri): Promise<Iterable<string>> {

src/language-server-stdio.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { MessageEmitter, MessageLogOptions, MessageWriter, registerLanguageHandl
55
import { RemoteLanguageClient } from './lang-handler';
66
import { FileLogger, StderrLogger } from './logging';
77
import { TypeScriptService, TypeScriptServiceOptions } from './typescript-service';
8-
import * as util from './util';
98

109
const packageJson = require('../package.json');
1110
const program = require('commander');
@@ -17,8 +16,6 @@ program
1716
.option('-l, --logfile [file]', 'log to this file')
1817
.parse(process.argv);
1918

20-
util.setStrict(program.strict);
21-
2219
const logger = program.logfile ? new FileLogger(program.logfile) : new StderrLogger();
2320
const options: TypeScriptServiceOptions & MessageLogOptions & RegisterLanguageHandlerOptions = {
2421
strict: program.strict,

src/language-server.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { FileLogger, StdioLogger } from './logging';
44
import { serve, ServeOptions } from './server';
55
import { TypeScriptService, TypeScriptServiceOptions } from './typescript-service';
6-
import * as util from './util';
76
const program = require('commander');
87
const packageJson = require('../package.json');
98

@@ -19,8 +18,6 @@ program
1918
.option('-l, --logfile [file]', 'log to this file')
2019
.parse(process.argv);
2120

22-
util.setStrict(program.strict);
23-
2421
const options: ServeOptions & TypeScriptServiceOptions = {
2522
clusterSize: program.cluster || numCPUs,
2623
lspPort: program.port || defaultLspPort,

src/memfs.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHo
7676
this.files.set(uri, content);
7777
}
7878
// Add to directory tree
79+
// TODO: convert this to use URIs.
7980
const filePath = uri2path(uri);
80-
const components = filePath.split('/').filter(c => c);
81+
const components = filePath.split(/[\/\\]/).filter(c => c);
8182
let node = this.rootNode;
8283
for (const [i, component] of components.entries()) {
8384
const n = node.children.get(component);
@@ -109,11 +110,12 @@ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHo
109110
* Returns the file content for the given URI.
110111
* Will throw an Error if no available in-memory.
111112
* Use FileSystemUpdater.ensure() to ensure that the file is available.
112-
*
113-
* TODO take overlay into account
114113
*/
115114
getContent(uri: string): string {
116-
let content = this.files.get(uri);
115+
let content = this.overlay.get(uri);
116+
if (content === undefined) {
117+
content = this.files.get(uri);
118+
}
117119
if (content === undefined) {
118120
content = typeScriptLibraries.get(uri2path(uri));
119121
}
@@ -129,7 +131,8 @@ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHo
129131
* @param path File path or URI (both absolute or relative file paths are accepted)
130132
*/
131133
fileExists(path: string): boolean {
132-
return this.readFileIfExists(path) !== undefined || this.files.has(path) || this.files.has(path2uri(this.path, path));
134+
const uri = path2uri(path);
135+
return this.overlay.has(uri) || this.files.has(uri) || typeScriptLibraries.has(path);
133136
}
134137

135138
/**
@@ -147,7 +150,7 @@ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHo
147150
* If there is no such file, returns undefined
148151
*/
149152
private readFileIfExists(path: string): string | undefined {
150-
const uri = path2uri(this.path, path);
153+
const uri = path2uri(path);
151154
let content = this.overlay.get(uri);
152155
if (content !== undefined) {
153156
return content;

src/project-manager.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ export class ProjectManager implements Disposable {
385385
return Observable.merge(
386386
// References with `import`
387387
Observable.from(info.importedFiles)
388-
.map(importedFile => ts.resolveModuleName(toUnixPath(importedFile.fileName), filePath, compilerOpt, config.moduleResolutionHost()))
388+
.map(importedFile => ts.resolveModuleName(importedFile.fileName, toUnixPath(filePath), compilerOpt, config.moduleResolutionHost()))
389389
// false means we didn't find a file defining the module. It
390390
// could still exist as an ambient module, which is why we
391391
// fetch global*.d.ts files.
@@ -416,7 +416,7 @@ export class ProjectManager implements Disposable {
416416
);
417417
})
418418
// Use same scheme, slashes, host for referenced URI as input file
419-
.map(filePath => path2uri('', filePath))
419+
.map(filePath => path2uri(filePath))
420420
// Don't cache errors
421421
.catch(err => {
422422
this.referencedFiles.delete(uri);
@@ -661,14 +661,10 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost {
661661
}
662662

663663
/**
664-
* @param fileName relative or absolute file path
664+
* @param fileName absolute file path
665665
*/
666-
getScriptVersion(fileName: string): string {
667-
668-
const uri = path2uri(this.rootPath, fileName);
669-
if (path.posix.isAbsolute(fileName) || path.isAbsolute(fileName)) {
670-
fileName = path.posix.relative(this.rootPath, toUnixPath(fileName));
671-
}
666+
getScriptVersion(filePath: string): string {
667+
const uri = path2uri(filePath);
672668
let version = this.versions.get(uri);
673669
if (!version) {
674670
version = 1;
@@ -678,18 +674,14 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost {
678674
}
679675

680676
/**
681-
* @param fileName relative or absolute file path
677+
* @param filePath absolute file path
682678
*/
683-
getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined {
684-
let exists = this.fs.fileExists(fileName);
685-
if (!exists) {
686-
fileName = path.posix.join(this.rootPath, fileName);
687-
exists = this.fs.fileExists(fileName);
688-
}
679+
getScriptSnapshot(filePath: string): ts.IScriptSnapshot | undefined {
680+
const exists = this.fs.fileExists(filePath);
689681
if (!exists) {
690682
return undefined;
691683
}
692-
return ts.ScriptSnapshot.fromString(this.fs.readFile(fileName));
684+
return ts.ScriptSnapshot.fromString(this.fs.readFile(filePath));
693685
}
694686

695687
getCurrentDirectory(): string {

src/symbols.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function locationUri(filePath: string): string {
1313
if (isTypeScriptLibrary(filePath)) {
1414
return 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/' + filePath.split(/[\/\\]/g).pop();
1515
}
16-
return path2uri('', filePath);
16+
return path2uri(filePath);
1717
}
1818

1919
/**

src/test/fs.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('fs.ts', () => {
3131

3232
// the project dir
3333
const projectDir = path.join(temporaryDir, 'project');
34-
rootUri = path2uri('', projectDir) + '/';
34+
rootUri = path2uri(projectDir) + '/';
3535
await fs.mkdir(projectDir);
3636
await fs.mkdir(path.join(projectDir, 'foo'));
3737
await fs.mkdir(path.join(projectDir, '@types'));
@@ -42,7 +42,7 @@ describe('fs.ts', () => {
4242
await fs.writeFile(path.join(projectDir, 'foo', 'bar.ts'), 'baz');
4343
await fs.writeFile(path.join(projectDir, '@types', 'diff', 'index.d.ts'), 'baz');
4444

45-
// global package is symolinked into project using npm link
45+
// global package is symlinked into project using npm link
4646
await fs.symlink(somePackageDir, path.join(projectDir, 'node_modules', 'some_package'), 'junction');
4747
fileSystem = new LocalFileSystem(rootUri);
4848
});

src/test/project-manager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('ProjectManager', () => {
8888
await projectManager.ensureAllFiles();
8989
});
9090
it('should resolve best configuration based on file name', () => {
91-
const config = projectManager.getParentConfiguration('/src/foo.ts');
91+
const config = projectManager.getParentConfiguration('file:///src/foo.ts');
9292
assert.isDefined(config);
9393
assert.equal('/tsconfig.json', config!.configFilePath);
9494
});
@@ -106,7 +106,7 @@ describe('ProjectManager', () => {
106106
await projectManager.ensureAllFiles();
107107
});
108108
it('should resolve best configuration based on file name', () => {
109-
const configs = Array.from(projectManager.getChildConfigurations('/foo')).map(config => config.configFilePath);
109+
const configs = Array.from(projectManager.getChildConfigurations('file:///foo')).map(config => config.configFilePath);
110110
assert.deepEqual(configs, [
111111
'/foo/bar/tsconfig.json',
112112
'/foo/baz/tsconfig.json'

src/test/typescript-service-helpers.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@ import { LanguageClient, RemoteLanguageClient } from '../lang-handler';
77
import { TextDocumentContentParams, WorkspaceFilesParams } from '../request-type';
88
import { SymbolLocationInformation } from '../request-type';
99
import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service';
10+
import { toUnixPath, uri2path } from '../util';
1011
import chaiAsPromised = require('chai-as-promised');
1112
import { apply } from 'json-patch';
1213
import { ISuiteCallbackContext, ITestCallbackContext } from 'mocha';
14+
1315
chai.use(chaiAsPromised);
1416
const assert = chai.assert;
1517

16-
/**
17-
* Enforcing strict mode to make tests pass on Windows
18-
*/
19-
import { setStrict, uri2path } from '../util';
20-
setStrict(true);
21-
2218
export interface TestContext {
2319

2420
/** TypeScript service under test */
@@ -2382,7 +2378,7 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
23822378
title: 'Add \'this.\' to unresolved variable.',
23832379
command: 'codeFix',
23842380
arguments: [{
2385-
fileName: uri2path(rootUri + 'a.ts'),
2381+
fileName: toUnixPath(uri2path(rootUri + 'a.ts')), // path only used by TS service
23862382
textChanges: [{
23872383
span: { start: 49, length: 13 },
23882384
newText: '\t\tthis.missingThis'

src/test/util-test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as assert from 'assert';
2-
import { getMatchingPropertyCount, getPropertyCount, isGlobalTSFile, isSymbolDescriptorMatch, JSONPTR } from '../util';
2+
import { getMatchingPropertyCount, getPropertyCount, isGlobalTSFile, isSymbolDescriptorMatch, JSONPTR, path2uri, uri2path } from '../util';
33

44
describe('util', () => {
55
describe('JSONPTR', () => {
@@ -129,4 +129,47 @@ describe('util', () => {
129129
assert.equal(isGlobalTSFile('/node_modules/@types/mocha/index.d.ts'), true);
130130
});
131131
});
132+
describe('path2uri()', () => {
133+
it('should throw an error if a non-absolute uri is passed in', () => {
134+
assert.throws(() => path2uri('baz/qux'));
135+
});
136+
it('should convert a Unix file path to a URI', () => {
137+
const uri = path2uri('/baz/qux');
138+
assert.equal(uri, 'file:///baz/qux');
139+
});
140+
it('should convert a Windows file path to a URI', () => {
141+
const uri = path2uri('C:\\baz\\qux');
142+
assert.equal(uri, 'file:///C:/baz/qux');
143+
});
144+
it('should encode special characters', () => {
145+
const uri = path2uri('/💩');
146+
assert.equal(uri, 'file:///%F0%9F%92%A9');
147+
});
148+
it('should encode unreserved special characters', () => {
149+
const uri = path2uri('/@baz');
150+
assert.equal(uri, 'file:///%40baz');
151+
});
152+
});
153+
describe('uri2path()', () => {
154+
it('should convert a Unix file URI to a file path', () => {
155+
const filePath = uri2path('file:///baz/qux');
156+
assert.equal(filePath, '/baz/qux');
157+
});
158+
it('should convert a Windows file URI to a file path', () => {
159+
const filePath = uri2path('file:///c:/baz/qux');
160+
assert.equal(filePath, 'c:\\baz\\qux');
161+
});
162+
it('should convert a Windows file URI with uppercase drive letter to a file path', () => {
163+
const filePath = uri2path('file:///C:/baz/qux');
164+
assert.equal(filePath, 'C:\\baz\\qux');
165+
});
166+
it('should decode special characters', () => {
167+
const filePath = uri2path('file:///%F0%9F%92%A9');
168+
assert.equal(filePath, '/💩');
169+
});
170+
it('should decode unreserved special characters', () => {
171+
const filePath = uri2path('file:///%40foo');
172+
assert.equal(filePath, '/@foo');
173+
});
174+
});
132175
});

0 commit comments

Comments
 (0)