Skip to content

Commit a1b41a7

Browse files
authored
Merge pull request #1153 from nojaf/track-diagnostics
Use custom string types to better determine what a path is
2 parents 2ab39a6 + ef8269f commit a1b41a7

File tree

11 files changed

+486
-205
lines changed

11 files changed

+486
-205
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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ 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+
// `path.normalize` ensures we can assume string is now NormalizedPath
30+
return filePath != null ? (path.normalize(filePath) as NormalizedPath) : null;
31+
}
32+
1233
type binaryName = "rescript-editor-analysis.exe" | "rescript-tools.exe";
1334

1435
const platformDir =
@@ -29,7 +50,7 @@ export const getLegacyBinaryProdPath = (b: binaryName) =>
2950

3051
export const getBinaryPath = (
3152
binaryName: "rescript-editor-analysis.exe" | "rescript-tools.exe",
32-
projectRootPath: string | null = null,
53+
projectRootPath: NormalizedPath | null = null,
3354
): string | null => {
3455
const binaryFromCompilerPackage = path.join(
3556
projectRootPath ?? "",
@@ -60,16 +81,23 @@ export const createFileInTempDir = (prefix = "", extension = "") => {
6081
};
6182

6283
export let findProjectRootOfFileInDir = (
63-
source: DocumentUri,
64-
): null | DocumentUri => {
65-
let dir = path.dirname(source);
84+
source: string,
85+
): NormalizedPath | null => {
86+
const normalizedSource = normalizePath(source);
87+
if (normalizedSource == null) {
88+
return null;
89+
}
90+
const dir = normalizePath(path.dirname(normalizedSource));
91+
if (dir == null) {
92+
return null;
93+
}
6694
if (
6795
fs.existsSync(path.join(dir, "rescript.json")) ||
6896
fs.existsSync(path.join(dir, "bsconfig.json"))
6997
) {
7098
return dir;
7199
} else {
72-
if (dir === source) {
100+
if (dir === normalizedSource) {
73101
// reached top
74102
return null;
75103
} else {

server/src/bsc-args/rewatch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ 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

2424
export async function getRewatchBscArgs(
2525
send: (msg: p.Message) => void,
26-
bscBinaryLocation: string | null,
27-
projectsFiles: Map<string, projectFiles>,
26+
bscBinaryLocation: utils.NormalizedPath | null,
27+
projectsFiles: Map<utils.NormalizedPath, projectFiles>,
2828
entry: IncrementallyCompiledFileInfo,
2929
): Promise<RewatchCompilerArgs | null> {
3030
const rewatchCacheEntry = entry.buildRewatch;
@@ -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: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
// OCaml binary.
44
import * as p from "vscode-languageserver-protocol";
55
import * as utils from "./utils";
6-
import { fileURLToPath } from "url";
76

87
export type fileCodeActions = { range: p.Range; codeAction: p.CodeAction };
98

109
export type filesCodeActions = {
11-
[key: string]: fileCodeActions[];
10+
[key: utils.FileURI]: fileCodeActions[];
1211
};
1312

1413
interface findCodeActionsConfig {
1514
diagnostic: p.Diagnostic;
1615
diagnosticMessage: string[];
17-
file: string;
16+
file: utils.FileURI;
1817
range: p.Range;
1918
addFoundActionsHere: filesCodeActions;
2019
}
@@ -190,7 +189,7 @@ interface codeActionExtractorConfig {
190189
line: string;
191190
index: number;
192191
array: string[];
193-
file: string;
192+
file: utils.FileURI;
194193
range: p.Range;
195194
diagnostic: p.Diagnostic;
196195
codeActions: filesCodeActions;
@@ -327,7 +326,7 @@ let handleUndefinedRecordFieldsAction = ({
327326
}: {
328327
recordFieldNames: string[];
329328
codeActions: filesCodeActions;
330-
file: string;
329+
file: utils.FileURI;
331330
range: p.Range;
332331
diagnostic: p.Diagnostic;
333332
todoValue: string;
@@ -631,7 +630,7 @@ let simpleAddMissingCases: codeActionExtractor = async ({
631630
.join("")
632631
.trim();
633632

634-
let filePath = fileURLToPath(file);
633+
let filePath = utils.uriToNormalizedPath(file);
635634

636635
let newSwitchCode = await utils.runAnalysisAfterSanityCheck(filePath, [
637636
"codemod",

server/src/find-runtime.ts

Lines changed: 13 additions & 5 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, normalizePath } from "./utils";
45

56
// Efficient parallel folder traversal to find node_modules directories
67
async function findNodeModulesDirs(
@@ -92,14 +93,18 @@ 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+
// We somewhat assume the user to pass down a normalized path, but we cannot be sure of this.
106+
const normalizedRuntimePath = normalizePath(compileInfo.runtime_path);
107+
return normalizedRuntimePath ? [normalizedRuntimePath] : [];
103108
}
104109
} catch {
105110
// Ignore errors, fallback to node_modules search
@@ -146,7 +151,10 @@ async function findRuntimePath(project: string): Promise<string[]> {
146151
}),
147152
).then((results) => results.flatMap((x) => x));
148153

149-
return rescriptRuntimeDirs.map((runtime) => resolve(runtime));
154+
return rescriptRuntimeDirs.map(
155+
// `resolve` ensures we can assume string is now NormalizedPath
156+
(runtime) => resolve(runtime) as NormalizedPath,
157+
);
150158
}
151159

152160
/**
@@ -156,7 +164,7 @@ async function findRuntimePath(project: string): Promise<string[]> {
156164
* (see getRuntimePathFromWorkspaceRoot in utils.ts).
157165
*/
158166
export async function findRescriptRuntimesInProject(
159-
project: string,
160-
): Promise<string[]> {
167+
project: NormalizedPath,
168+
): Promise<NormalizedPath[]> {
161169
return await findRuntimePath(project);
162170
}

0 commit comments

Comments
 (0)