Skip to content

Commit b142a63

Browse files
committed
More usage of NormalizedPath
1 parent add5596 commit b142a63

File tree

9 files changed

+84
-36
lines changed

9 files changed

+84
-36
lines changed

client/src/commands/code_analysis.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
OutputChannel,
1515
StatusBarItem,
1616
} from "vscode";
17-
import { findProjectRootOfFileInDir, getBinaryPath } from "../utils";
17+
import {
18+
findProjectRootOfFileInDir,
19+
getBinaryPath,
20+
NormalizedPath,
21+
} from "../utils";
1822

1923
export let statusBarItem = {
2024
setToStopText: (codeAnalysisRunningStatusBarItem: StatusBarItem) => {
@@ -208,7 +212,7 @@ export const runCodeAnalysisWithReanalyze = (
208212
let currentDocument = window.activeTextEditor.document;
209213
let cwd = targetDir ?? path.dirname(currentDocument.uri.fsPath);
210214

211-
let projectRootPath: string | null = findProjectRootOfFileInDir(
215+
let projectRootPath: NormalizedPath | null = findProjectRootOfFileInDir(
212216
currentDocument.uri.fsPath,
213217
);
214218

client/src/commands/dump_debug.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createFileInTempDir,
1212
findProjectRootOfFileInDir,
1313
getBinaryPath,
14+
NormalizedPath,
1415
} from "../utils";
1516
import * as path from "path";
1617

@@ -136,7 +137,8 @@ export const dumpDebug = async (
136137
const { line: endLine, character: endChar } = editor.selection.end;
137138
const filePath = editor.document.uri.fsPath;
138139

139-
let projectRootPath: string | null = findProjectRootOfFileInDir(filePath);
140+
let projectRootPath: NormalizedPath | null =
141+
findProjectRootOfFileInDir(filePath);
140142
const binaryPath = getBinaryPath(
141143
"rescript-editor-analysis.exe",
142144
projectRootPath,

client/src/utils.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import { DocumentUri } from "vscode-languageclient";
99
* to the server itself.
1010
*/
1111

12+
/**
13+
* Branded type for normalized file paths.
14+
*
15+
* All paths should be normalized to ensure consistent lookups and prevent
16+
* path format mismatches (e.g., trailing slashes, relative vs absolute paths).
17+
*
18+
* Use `normalizePath()` to convert a regular path to a `NormalizedPath`.
19+
*/
20+
export type NormalizedPath = string & { __brand: "NormalizedPath" };
21+
22+
/**
23+
* Normalizes a file path and returns it as a `NormalizedPath`.
24+
*
25+
* @param filePath - The path to normalize (can be null)
26+
* @returns The normalized path, or null if input was null
27+
*/
28+
export function normalizePath(filePath: string | null): NormalizedPath | null {
29+
return filePath != null ? (path.normalize(filePath) as NormalizedPath) : null;
30+
}
31+
1232
type binaryName = "rescript-editor-analysis.exe" | "rescript-tools.exe";
1333

1434
const platformDir =
@@ -29,7 +49,7 @@ export const getLegacyBinaryProdPath = (b: binaryName) =>
2949

3050
export const getBinaryPath = (
3151
binaryName: "rescript-editor-analysis.exe" | "rescript-tools.exe",
32-
projectRootPath: string | null = null,
52+
projectRootPath: NormalizedPath | null = null,
3353
): string | null => {
3454
const binaryFromCompilerPackage = path.join(
3555
projectRootPath ?? "",
@@ -59,15 +79,24 @@ export const createFileInTempDir = (prefix = "", extension = "") => {
5979
return path.join(os.tmpdir(), tempFileName);
6080
};
6181

62-
export let findProjectRootOfFileInDir = (source: string): null | string => {
63-
let dir = path.dirname(source);
82+
export let findProjectRootOfFileInDir = (
83+
source: string,
84+
): NormalizedPath | null => {
85+
const normalizedSource = normalizePath(source);
86+
if (normalizedSource == null) {
87+
return null;
88+
}
89+
const dir = normalizePath(path.dirname(normalizedSource));
90+
if (dir == null) {
91+
return null;
92+
}
6493
if (
6594
fs.existsSync(path.join(dir, "rescript.json")) ||
6695
fs.existsSync(path.join(dir, "bsconfig.json"))
6796
) {
6897
return dir;
6998
} else {
70-
if (dir === source) {
99+
if (dir === normalizedSource) {
71100
// reached top
72101
return null;
73102
} else {

server/src/bsc-args/rewatch.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type RewatchCompilerArgs = {
1717

1818
async function getRuntimePath(
1919
entry: IncrementallyCompiledFileInfo,
20-
): Promise<string | null> {
20+
): Promise<utils.NormalizedPath | null> {
2121
return utils.getRuntimePathFromWorkspaceRoot(entry.project.workspaceRootPath);
2222
}
2323

@@ -103,7 +103,8 @@ export async function getRewatchBscArgs(
103103
includePrerelease: true,
104104
})
105105
) {
106-
let rescriptRuntime: string | null = await getRuntimePath(entry);
106+
let rescriptRuntime: utils.NormalizedPath | null =
107+
await getRuntimePath(entry);
107108

108109
if (rescriptRuntime !== null) {
109110
env["RESCRIPT_RUNTIME"] = rescriptRuntime;

server/src/codeActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as utils from "./utils";
77
export type fileCodeActions = { range: p.Range; codeAction: p.CodeAction };
88

99
export type filesCodeActions = {
10-
[key: string]: fileCodeActions[];
10+
[key: utils.FileURI]: fileCodeActions[];
1111
};
1212

1313
interface findCodeActionsConfig {
@@ -326,7 +326,7 @@ let handleUndefinedRecordFieldsAction = ({
326326
}: {
327327
recordFieldNames: string[];
328328
codeActions: filesCodeActions;
329-
file: string;
329+
file: utils.FileURI;
330330
range: p.Range;
331331
diagnostic: p.Diagnostic;
332332
todoValue: string;

server/src/find-runtime.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readdir, stat as statAsync, readFile } from "fs/promises";
22
import { join, resolve } from "path";
33
import { compilerInfoPartialPath } from "./constants";
4+
import { NormalizedPath } from "./utils";
45

56
// Efficient parallel folder traversal to find node_modules directories
67
async function findNodeModulesDirs(
@@ -92,14 +93,16 @@ async function findRescriptRuntimeInAlternativeLayout(
9293
return results;
9394
}
9495

95-
async function findRuntimePath(project: string): Promise<string[]> {
96+
async function findRuntimePath(
97+
project: NormalizedPath,
98+
): Promise<NormalizedPath[]> {
9699
// Try a compiler-info.json file first
97100
const compilerInfo = resolve(project, compilerInfoPartialPath);
98101
try {
99102
const contents = await readFile(compilerInfo, "utf8");
100103
const compileInfo: { runtime_path?: string } = JSON.parse(contents);
101104
if (compileInfo && compileInfo.runtime_path) {
102-
return [compileInfo.runtime_path];
105+
return [compileInfo.runtime_path as NormalizedPath];
103106
}
104107
} catch {
105108
// Ignore errors, fallback to node_modules search
@@ -146,12 +149,16 @@ async function findRuntimePath(project: string): Promise<string[]> {
146149
}),
147150
).then((results) => results.flatMap((x) => x));
148151

149-
return rescriptRuntimeDirs.map((runtime) => resolve(runtime));
152+
return rescriptRuntimeDirs.map(
153+
(runtime) => resolve(runtime) as NormalizedPath,
154+
);
150155
}
151156

152-
function findRuntimeCached(): (project: string) => Promise<string[]> {
153-
const cache = new Map<string, string[]>();
154-
return async (project: string) => {
157+
function findRuntimeCached(): (
158+
project: NormalizedPath,
159+
) => Promise<NormalizedPath[]> {
160+
const cache = new Map<NormalizedPath, NormalizedPath[]>();
161+
return async (project: NormalizedPath) => {
155162
if (cache.has(project)) {
156163
return cache.get(project)!;
157164
}

server/src/incrementalCompilation.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as path from "path";
22
import fs from "fs";
33
import * as utils from "./utils";
4-
import readline from "readline";
54
import { performance } from "perf_hooks";
65
import * as p from "vscode-languageserver-protocol";
76
import * as cp from "node:child_process";
@@ -41,9 +40,9 @@ export type IncrementallyCompiledFileInfo = {
4140
/** Namespaced module name of the source file. */
4241
moduleNameNamespaced: string;
4342
/** Path to where the incremental file is saved. */
44-
incrementalFilePath: string;
43+
incrementalFilePath: NormalizedPath;
4544
/** Location of the original type file. */
46-
originalTypeFileLocation: string;
45+
originalTypeFileLocation: NormalizedPath;
4746
};
4847
buildSystem: "bsb" | "rewatch";
4948
/** Cache for build.ninja assets. */
@@ -55,7 +54,7 @@ export type IncrementallyCompiledFileInfo = {
5554
} | null;
5655
/** Cache for rewatch compiler args. */
5756
buildRewatch: {
58-
lastFile: string;
57+
lastFile: NormalizedPath;
5958
compilerArgs: RewatchCompilerArgs;
6059
} | null;
6160
/** Info of the currently active incremental compilation. `null` if no incremental compilation is active. */
@@ -88,8 +87,9 @@ const incrementallyCompiledFileInfo: Map<
8887
NormalizedPath,
8988
IncrementallyCompiledFileInfo
9089
> = new Map();
91-
const hasReportedFeatureFailedError: Set<string> = new Set();
92-
const originalTypeFileToFilePath: Map<string, NormalizedPath> = new Map();
90+
const hasReportedFeatureFailedError: Set<NormalizedPath> = new Set();
91+
const originalTypeFileToFilePath: Map<NormalizedPath, NormalizedPath> =
92+
new Map();
9393

9494
export function incrementalCompilationFileChanged(changedPath: NormalizedPath) {
9595
const filePath = originalTypeFileToFilePath.get(changedPath);
@@ -311,12 +311,12 @@ function triggerIncrementalCompilationOfFile(
311311
projectRootPath,
312312
c.compilerDirPartialPath,
313313
path.relative(projectRootPath, filePath),
314-
);
314+
) as NormalizedPath;
315315

316316
const parsed = path.parse(originalTypeFileLocation);
317317
parsed.ext = ext === ".res" ? ".cmt" : ".cmti";
318318
parsed.base = "";
319-
originalTypeFileLocation = path.format(parsed);
319+
originalTypeFileLocation = path.format(parsed) as NormalizedPath;
320320

321321
incrementalFileCacheEntry = {
322322
file: {
@@ -326,7 +326,10 @@ function triggerIncrementalCompilationOfFile(
326326
moduleNameNamespaced,
327327
sourceFileName: moduleName + ext,
328328
sourceFilePath: filePath,
329-
incrementalFilePath: path.join(incrementalFolderPath, moduleName + ext),
329+
incrementalFilePath: path.join(
330+
incrementalFolderPath,
331+
moduleName + ext,
332+
) as NormalizedPath,
330333
},
331334
project: {
332335
workspaceRootPath,

server/src/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import { onErrorReported } from "./errorReporter";
2727
import * as ic from "./incrementalCompilation";
2828
import config, { extensionConfiguration } from "./config";
2929
import { projectsFiles } from "./projectFiles";
30+
import { NormalizedPath } from "./utils";
3031

3132
// Absolute paths to all the workspace folders
3233
// Configured during the initialize request
33-
export const workspaceFolders = new Set<string>();
34+
export const workspaceFolders = new Set<NormalizedPath>();
3435

3536
// This holds client capabilities specific to our extension, and not necessarily
3637
// related to the LS protocol. It's for enabling/disabling features that might
@@ -75,7 +76,7 @@ const projectCompilationStates: Map<string, ProjectCompilationState> =
7576

7677
type CompilationStatusPayload = {
7778
project: string;
78-
projectRootPath: string;
79+
projectRootPath: NormalizedPath;
7980
status: "compiling" | "success" | "error" | "warning";
8081
errorCount: number;
8182
warningCount: number;
@@ -921,7 +922,7 @@ async function codeAction(msg: p.RequestMessage): Promise<p.ResponseMessage> {
921922
// Check local code actions coming from the diagnostics, or from incremental compilation.
922923
let localResults: v.CodeAction[] = [];
923924
const fromDiagnostics =
924-
codeActionsFromDiagnostics[params.textDocument.uri] ?? [];
925+
codeActionsFromDiagnostics[params.textDocument.uri as utils.FileURI] ?? [];
925926
const fromIncrementalCompilation =
926927
ic.getCodeActionsFromIncrementalCompilation(filePath) ?? [];
927928
[...fromDiagnostics, ...fromIncrementalCompilation].forEach(

server/src/utils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ export function uriToNormalizedPath(uri: FileURI): NormalizedPath {
7474
let tempFilePrefix = "rescript_format_file_" + process.pid + "_";
7575
let tempFileId = 0;
7676

77-
export let createFileInTempDir = (extension = "") => {
77+
export let createFileInTempDir = (extension = ""): NormalizedPath => {
7878
let tempFileName = tempFilePrefix + tempFileId + extension;
7979
tempFileId = tempFileId + 1;
80-
return path.join(os.tmpdir(), tempFileName);
80+
return path.join(os.tmpdir(), tempFileName) as NormalizedPath;
8181
};
8282

8383
let findProjectRootOfFileInDir = (
@@ -501,23 +501,24 @@ export function computeWorkspaceRootPathFromLockfile(
501501
}
502502

503503
// Shared cache: key is either workspace root path or project root path
504-
const runtimePathCache = new Map<string, string | null>();
504+
const runtimePathCache = new Map<NormalizedPath, NormalizedPath | null>();
505505

506506
/**
507507
* Gets the runtime path from a workspace root path.
508508
* This function is cached per workspace root path.
509509
*/
510510
export async function getRuntimePathFromWorkspaceRoot(
511511
workspaceRootPath: NormalizedPath,
512-
): Promise<string | null> {
512+
): Promise<NormalizedPath | null> {
513513
// Check cache first
514514
if (runtimePathCache.has(workspaceRootPath)) {
515515
return runtimePathCache.get(workspaceRootPath)!;
516516
}
517517

518518
// Compute and cache
519-
let rescriptRuntime: string | null =
520-
config.extensionConfiguration.runtimePath ?? null;
519+
let rescriptRuntime: NormalizedPath | null = normalizePath(
520+
config.extensionConfiguration.runtimePath ?? null,
521+
);
521522

522523
if (rescriptRuntime !== null) {
523524
runtimePathCache.set(workspaceRootPath, rescriptRuntime);
@@ -539,7 +540,7 @@ export async function getRuntimePathFromWorkspaceRoot(
539540
*/
540541
export async function getRuntimePathFromProjectRoot(
541542
projectRootPath: NormalizedPath | null,
542-
): Promise<string | null> {
543+
): Promise<NormalizedPath | null> {
543544
if (projectRootPath == null) {
544545
return null;
545546
}

0 commit comments

Comments
 (0)