Skip to content

Commit 59f5b02

Browse files
authored
Typed path prep bugfixes (#64544)
1 parent 82f0546 commit 59f5b02

282 files changed

Lines changed: 3368 additions & 1034 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/typescript/src/api/async/api.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
340340
readonly client: Client;
341341
/** @internal */
342342
readonly sourceFileCache: SourceFileCache<Symbol>;
343-
private toPath: ((fileName: string) => Path) | undefined;
343+
private toPath: ((fileName: string, basePath?: string) => Path) | undefined;
344344
private currentDirectory: string | undefined;
345345
private readonly decoder = new Wtf8Decoder();
346346
private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined;
@@ -442,7 +442,7 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
442442
const currentDirectory = response.currentDirectory;
443443
this.getCanonicalFileNameWorker = getCanonicalFileName;
444444
this.currentDirectory = currentDirectory;
445-
this.toPath = (fileName: string) => toPath(fileName, currentDirectory, getCanonicalFileName) as Path;
445+
this.toPath = (fileName: string, basePath = currentDirectory) => toPath(fileName, basePath, getCanonicalFileName) as Path;
446446
this.initialized = true;
447447
}
448448
catch (error) {
@@ -1023,7 +1023,7 @@ export class Snapshot {
10231023
readonly id: number;
10241024
readonly operation: SnapshotOperation;
10251025
private projectMap: Map<ProjectId, Project>;
1026-
private toPath: (fileName: string) => Path;
1026+
private toPath: (fileName: string, basePath?: string) => Path;
10271027
private readonly api: API<boolean>;
10281028
private disposed: boolean = false;
10291029
private disposePromise: Promise<void> | undefined;
@@ -1037,7 +1037,7 @@ export class Snapshot {
10371037
return this.api.client;
10381038
}
10391039

1040-
constructor(data: CreateSnapshotResponse, toPath: (fileName: string) => Path, api: API<boolean>, onDispose: () => void, updateSnapshot: SnapshotUpdater, baseSnapshot?: Snapshot) {
1040+
constructor(data: CreateSnapshotResponse, toPath: (fileName: string, basePath?: string) => Path, api: API<boolean>, onDispose: () => void, updateSnapshot: SnapshotUpdater, baseSnapshot?: Snapshot) {
10411041
this.id = data.snapshot;
10421042
this.api = api;
10431043
this.toPath = toPath;
@@ -1527,7 +1527,7 @@ export class Project<Id extends ProjectId = ProjectId> {
15271527
readonly languageService: LanguageService;
15281528
private snapshotId: number;
15291529

1530-
constructor(data: ProjectResponse, snapshotId: number, toPath: (fileName: string) => Path, api: API<boolean>, snapshotRegistry: SnapshotObjectRegistry) {
1530+
constructor(data: ProjectResponse, snapshotId: number, toPath: (fileName: string, basePath?: string) => Path, api: API<boolean>, snapshotRegistry: SnapshotObjectRegistry) {
15311531
this.id = data.id as Id;
15321532
this.api = api;
15331533
this.configFileName = data.configFileName;
@@ -1672,7 +1672,7 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
16721672
readonly snapshotId: number;
16731673
readonly id: Id;
16741674
private readonly project: Project<Id>;
1675-
private readonly toPath: (fileName: string) => Path;
1675+
private readonly toPath: (fileName: string, basePath?: string) => Path;
16761676
private readonly decoder = new Wtf8Decoder();
16771677
private readonly sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
16781678
private ownedSnapshot: Snapshot | undefined;
@@ -1686,7 +1686,7 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
16861686
return this.project.api.sourceFileCache;
16871687
}
16881688

1689-
constructor(snapshotId: number, project: Project<Id>, toPath: (fileName: string) => Path) {
1689+
constructor(snapshotId: number, project: Project<Id>, toPath: (fileName: string, basePath?: string) => Path) {
16901690
this.snapshotId = snapshotId;
16911691
this.id = project.id;
16921692
this.project = project;
@@ -1730,7 +1730,7 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
17301730

17311731
async getSourceFile(file: DocumentIdentifier): Promise<SourceFile | undefined> {
17321732
const fileName = resolveFileName(file);
1733-
const path = this.toPath(fileName);
1733+
const path = this.toPath(fileName, this.project.currentDirectory);
17341734

17351735
// Check if we already have a retained cache entry for this (snapshot, project) pair
17361736
const retained = this.sourceFileCache.getRetained(path, this.snapshotId, this.project.id);
@@ -1842,7 +1842,7 @@ export class Program<Id extends ProjectId = ProjectId> implements FormatDiagnost
18421842
* `Program` instance.
18431843
*/
18441844
getSourceFileMetadata(file: DocumentIdentifier): Promise<SourceFileMetadata | undefined> {
1845-
return this.getSourceFileMetadataByPath(this.toPath(resolveFileName(file)));
1845+
return this.getSourceFileMetadataByPath(this.toPath(resolveFileName(file), this.project.currentDirectory));
18461846
}
18471847

18481848
/**

‎packages/typescript/src/api/path.ts‎

Lines changed: 210 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ function getEncodedRootLength(path: string): number {
8888
return p1 + 1; // UNC: "//server/" or "\\server\"
8989
}
9090

91+
// Dynamic/virtual compiler file name.
92+
if (ch0 === 94 /* ^ */ && path.charCodeAt(1) === CharacterCodesSlash) {
93+
if (path.startsWith(dynamicURIFileNamePrefix)) {
94+
const schemeEnd = path.indexOf(directorySeparator, dynamicURIFileNamePrefix.length);
95+
if (schemeEnd !== -1) {
96+
const authorityEnd = path.indexOf(directorySeparator, schemeEnd + 1);
97+
return authorityEnd === -1 ? path.length : authorityEnd + 1;
98+
}
99+
}
100+
return 2;
101+
}
102+
91103
// DOS
92104
if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodesColon) {
93105
const ch2 = path.charCodeAt(2);
@@ -109,7 +121,7 @@ function getEncodedRootLength(path: string): number {
109121
const scheme = path.slice(0, schemeEnd);
110122
const authority = path.slice(authorityStart, authorityEnd);
111123
if (
112-
scheme === "file" && (authority === "" || authority === "localhost") &&
124+
scheme.toLowerCase() === "file" && (authority === "" || authority.toLowerCase() === "localhost") &&
113125
isVolumeCharacter(path.charCodeAt(authorityEnd + 1))
114126
) {
115127
const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
@@ -391,7 +403,9 @@ export function toPath(fileName: string, basePath: string | undefined, getCanoni
391403
const nonCanonicalizedPath = isRootedDiskPath(fileName)
392404
? normalizePath(fileName)
393405
: getNormalizedAbsolutePath(fileName, basePath);
394-
return getCanonicalFileName(nonCanonicalizedPath);
406+
return nonCanonicalizedPath.startsWith(dynamicURIFileNamePrefix)
407+
? nonCanonicalizedPath
408+
: getCanonicalFileName(nonCanonicalizedPath);
395409
}
396410

397411
/**
@@ -410,6 +424,11 @@ function toLowerCase(s: string): string {
410424
}
411425

412426
const bundledScheme = "bundled:///";
427+
const dynamicURIFileNamePrefix = "^/~ts-uri~/";
428+
const dynamicURIPathSegmentEscapePrefix = "~ts-uri-escape~";
429+
const dynamicURIModuleSpecifierEscapePrefix = "~ts-uri-spec~";
430+
const dynamicURINoPathEscapePrefix = "~ts-uri-no-path~";
431+
const dynamicURIPathSegmentEscapeRegExp = /(?:^|\/)(?:\.{1,2}(?:\/|$)|~ts-uri-escape~|~ts-uri-spec~|~ts-uri-no-path~)/;
413432

414433
/**
415434
* Returns true if the path refers to a bundled library file.
@@ -493,8 +512,8 @@ export function fileNameToDocumentURI(fileName: string): string {
493512

494513
// Dynamic/virtual files (untitled, vscode-vfs, etc.) need special handling
495514
if (isDynamicFileName(fileName)) {
496-
// Format: ^/scheme/authority/path
497-
const withoutPrefix = fileName.substring(2); // Remove "^/"
515+
const encoded = fileName.startsWith(dynamicURIFileNamePrefix);
516+
const withoutPrefix = fileName.substring(encoded ? dynamicURIFileNamePrefix.length : 2);
498517
const firstSlash = withoutPrefix.indexOf("/");
499518
if (firstSlash === -1) {
500519
throw new Error("invalid file name: " + fileName);
@@ -506,11 +525,20 @@ export function fileNameToDocumentURI(fileName: string): string {
506525
if (secondSlash === -1) {
507526
throw new Error("invalid file name: " + fileName);
508527
}
509-
const authority = rest.substring(0, secondSlash);
510-
const path = rest.substring(secondSlash + 1);
528+
const encodedAuthority = rest.substring(0, secondSlash);
529+
const hasAuthority = encodedAuthority !== "ts-nul-authority";
530+
const authority = encoded ? decodeDynamicURIPathSegment(encodedAuthority) : encodedAuthority;
531+
const encodedPath = rest.substring(secondSlash + 1);
532+
if (encoded && hasAuthority) {
533+
const suffix = decodeDynamicURINoPath(encodedPath);
534+
if (suffix !== undefined) {
535+
return scheme + "://" + authority + suffix;
536+
}
537+
}
538+
const path = encoded ? decodeDynamicURIPath(encodedPath) : encodedPath;
511539

512540
// ts-nul-authority is a placeholder for URIs without an authority
513-
if (authority === "ts-nul-authority") {
541+
if (!hasAuthority) {
514542
return scheme + ":" + path;
515543
}
516544
return scheme + "://" + authority + "/" + path;
@@ -585,17 +613,188 @@ export function documentURIToFileName(uri: string): string {
585613

586614
const scheme = uri.substring(0, colonIndex);
587615
let path = uri.substring(colonIndex + 1);
616+
let suffix = "";
617+
const suffixStart = path.search(/[?#]/);
618+
if (suffixStart !== -1) {
619+
suffix = path.substring(suffixStart);
620+
path = path.substring(0, suffixStart);
621+
}
588622

589623
let authority = "ts-nul-authority";
624+
let hasAuthority = false;
625+
let hasPath = true;
590626
if (path.startsWith("//")) {
627+
hasAuthority = true;
591628
const rest = path.substring(2);
592629
const slashIndex = rest.indexOf("/");
593630
if (slashIndex === -1) {
594-
throw new Error("invalid URI: " + uri);
631+
authority = rest;
632+
path = "";
633+
hasPath = false;
634+
}
635+
else {
636+
authority = rest.substring(0, slashIndex);
637+
path = rest.substring(slashIndex + 1);
638+
}
639+
}
640+
641+
let encodedAuthority = authority;
642+
if (hasAuthority) {
643+
encodedAuthority = authority === "ts-nul-authority"
644+
? forceEncodeDynamicURIPathSegment(authority, false)
645+
: encodeDynamicURIPath(authority);
646+
}
647+
const encodedPath = hasPath
648+
? encodeDynamicURIPathWithSuffix(path, suffix)
649+
: encodeDynamicURINoPath(suffix);
650+
return dynamicURIFileNamePrefix + scheme + "/" + encodedAuthority + "/" + encodedPath;
651+
}
652+
653+
function encodeDynamicURIPath(path: string, preserveFinalExtension = true): string {
654+
if (!dynamicURIPathNeedsEncoding(path) && !isRootedDiskPath(path)) {
655+
return path;
656+
}
657+
const segments = path.split("/");
658+
for (let i = 0; i < segments.length; i++) {
659+
segments[i] = encodeDynamicURIPathSegment(segments[i], preserveFinalExtension && i === segments.length - 1);
660+
}
661+
if (isRootedDiskPath(segments.join("/"))) {
662+
segments[0] = forceEncodeDynamicURIPathSegment(segments[0], segments.length === 1 && preserveFinalExtension);
663+
}
664+
return segments.join("/");
665+
}
666+
667+
function encodeDynamicURIPathWithSuffix(path: string, suffix: string): string {
668+
if (suffix === "") {
669+
return encodeDynamicURIPath(path);
670+
}
671+
const slash = path.lastIndexOf("/");
672+
const before = slash === -1 ? "" : encodeDynamicURIPath(path.substring(0, slash), false) + "/";
673+
return before + forceEncodeDynamicURIPathSegmentWithSuffix(path.substring(slash + 1), suffix);
674+
}
675+
676+
function dynamicURIPathNeedsEncoding(path: string): boolean {
677+
return path === "" ||
678+
path.startsWith("/") ||
679+
path.endsWith("/") ||
680+
path.includes("//") ||
681+
path.includes("\\") ||
682+
dynamicURIPathSegmentEscapeRegExp.test(path);
683+
}
684+
685+
function dynamicURIPathSegmentNeedsEncoding(segment: string): boolean {
686+
return segment === "" ||
687+
segment === "." ||
688+
segment === ".." ||
689+
segment.startsWith(dynamicURIPathSegmentEscapePrefix) ||
690+
segment.startsWith(dynamicURIModuleSpecifierEscapePrefix) ||
691+
segment.startsWith(dynamicURINoPathEscapePrefix) ||
692+
segment.includes("\\");
693+
}
694+
695+
function encodeDynamicURIPathSegment(segment: string, preserveExtension: boolean): string {
696+
return dynamicURIPathSegmentNeedsEncoding(segment)
697+
? forceEncodeDynamicURIPathSegment(segment, preserveExtension)
698+
: segment;
699+
}
700+
701+
function forceEncodeDynamicURIPathSegment(segment: string, preserveExtension: boolean): string {
702+
let extension = "";
703+
if (preserveExtension && segment !== "." && segment !== "..") {
704+
[segment, extension] = splitDynamicURIFileExtension(segment);
705+
}
706+
return dynamicURIPathSegmentEscapePrefix + encodeDynamicURIHex(segment) + "~" + extension;
707+
}
708+
709+
function forceEncodeDynamicURIPathSegmentWithSuffix(segment: string, suffix: string): string {
710+
const [base, extension] = splitDynamicURIFileExtension(segment);
711+
return forceEncodeDynamicURIPathSegment(base + "\0" + suffix, false) + extension;
712+
}
713+
714+
function splitDynamicURIFileExtension(segment: string): [base: string, extension: string] {
715+
const extension = getDynamicURIFileExtension(segment);
716+
return extension === ""
717+
? [segment, extension]
718+
: [segment.substring(0, segment.length - extension.length), extension];
719+
}
720+
721+
function getDynamicURIFileExtension(segment: string): string {
722+
const baseStart = segment.lastIndexOf("\\") + 1;
723+
for (const extension of [".d.ts", ".d.mts", ".d.cts"]) {
724+
if (segment.endsWith(extension) && segment.length - extension.length >= baseStart) {
725+
return extension;
726+
}
727+
}
728+
if (segment.endsWith(".ts")) {
729+
const declaration = segment.indexOf(".d.", baseStart);
730+
if (declaration !== -1) {
731+
return segment.substring(declaration);
595732
}
596-
authority = rest.substring(0, slashIndex);
597-
path = rest.substring(slashIndex + 1);
598733
}
734+
const dot = segment.lastIndexOf(".");
735+
return dot >= baseStart ? segment.substring(dot) : "";
736+
}
737+
738+
function encodeDynamicURIHex(text: string): string {
739+
let hex = "";
740+
const encoded = encodeURIComponent(text);
741+
for (let i = 0; i < encoded.length; i++) {
742+
if (encoded.charCodeAt(i) === CharacterCodesPercent) {
743+
hex += encoded.slice(i + 1, i + 3).toLowerCase();
744+
i += 2;
745+
}
746+
else {
747+
hex += encoded.charCodeAt(i).toString(16).padStart(2, "0");
748+
}
749+
}
750+
return hex;
751+
}
599752

600-
return "^/" + scheme + "/" + authority + "/" + path;
753+
function decodeDynamicURIPath(path: string): string {
754+
return path.includes(dynamicURIPathSegmentEscapePrefix)
755+
? path.split("/").map(decodeDynamicURIPathSegment).join("/")
756+
: path;
757+
}
758+
759+
function decodeDynamicURIPathSegment(segment: string): string {
760+
if (!segment.startsWith(dynamicURIPathSegmentEscapePrefix)) {
761+
return segment;
762+
}
763+
const separator = segment.indexOf("~", dynamicURIPathSegmentEscapePrefix.length);
764+
if (separator === -1) {
765+
return segment;
766+
}
767+
const encoded = segment.slice(dynamicURIPathSegmentEscapePrefix.length, separator);
768+
const extension = segment.slice(separator + 1);
769+
const decoded = decodeDynamicURIHex(encoded);
770+
if (decoded === undefined) {
771+
return segment;
772+
}
773+
const suffix = decoded.indexOf("\0");
774+
return suffix === -1
775+
? decoded + extension
776+
: decoded.substring(0, suffix) + extension + decoded.substring(suffix + 1);
777+
}
778+
779+
function encodeDynamicURINoPath(suffix: string): string {
780+
return dynamicURINoPathEscapePrefix + encodeDynamicURIHex(suffix) + "~";
781+
}
782+
783+
function decodeDynamicURINoPath(path: string): string | undefined {
784+
if (!path.startsWith(dynamicURINoPathEscapePrefix) || !path.endsWith("~")) {
785+
return undefined;
786+
}
787+
return decodeDynamicURIHex(path.substring(dynamicURINoPathEscapePrefix.length, path.length - 1));
788+
}
789+
790+
function decodeDynamicURIHex(encoded: string): string | undefined {
791+
if (encoded.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(encoded)) {
792+
return undefined;
793+
}
794+
try {
795+
return decodeURIComponent(encoded.replace(/../g, value => `%${value}`));
796+
}
797+
catch {
798+
return undefined;
799+
}
601800
}

0 commit comments

Comments
 (0)