Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions client/src/commands/code_analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
OutputChannel,
StatusBarItem,
} from "vscode";
import { findProjectRootOfFileInDir, getBinaryPath } from "../utils";
import {
findProjectRootOfFileInDir,
getBinaryPath,
NormalizedPath,
} from "../utils";

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

let projectRootPath: string | null = findProjectRootOfFileInDir(
let projectRootPath: NormalizedPath | null = findProjectRootOfFileInDir(
currentDocument.uri.fsPath,
);

Expand Down
4 changes: 3 additions & 1 deletion client/src/commands/dump_debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createFileInTempDir,
findProjectRootOfFileInDir,
getBinaryPath,
NormalizedPath,
} from "../utils";
import * as path from "path";

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

let projectRootPath: string | null = findProjectRootOfFileInDir(filePath);
let projectRootPath: NormalizedPath | null =
findProjectRootOfFileInDir(filePath);
const binaryPath = getBinaryPath(
"rescript-editor-analysis.exe",
projectRootPath,
Expand Down
38 changes: 33 additions & 5 deletions client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ import { DocumentUri } from "vscode-languageclient";
* to the server itself.
*/

/**
* Branded type for normalized file paths.
*
* All paths should be normalized to ensure consistent lookups and prevent
* path format mismatches (e.g., trailing slashes, relative vs absolute paths).
*
* Use `normalizePath()` to convert a regular path to a `NormalizedPath`.
*/
export type NormalizedPath = string & { __brand: "NormalizedPath" };

/**
* Normalizes a file path and returns it as a `NormalizedPath`.
*
* @param filePath - The path to normalize (can be null)
* @returns The normalized path, or null if input was null
*/
export function normalizePath(filePath: string | null): NormalizedPath | null {
// `path.normalize` ensures we can assume string is now NormalizedPath
return filePath != null ? (path.normalize(filePath) as NormalizedPath) : null;
}

type binaryName = "rescript-editor-analysis.exe" | "rescript-tools.exe";

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

export const getBinaryPath = (
binaryName: "rescript-editor-analysis.exe" | "rescript-tools.exe",
projectRootPath: string | null = null,
projectRootPath: NormalizedPath | null = null,
): string | null => {
const binaryFromCompilerPackage = path.join(
projectRootPath ?? "",
Expand Down Expand Up @@ -60,16 +81,23 @@ export const createFileInTempDir = (prefix = "", extension = "") => {
};

export let findProjectRootOfFileInDir = (
source: DocumentUri,
): null | DocumentUri => {
let dir = path.dirname(source);
source: string,
): NormalizedPath | null => {
const normalizedSource = normalizePath(source);
if (normalizedSource == null) {
return null;
}
const dir = normalizePath(path.dirname(normalizedSource));
if (dir == null) {
return null;
}
if (
fs.existsSync(path.join(dir, "rescript.json")) ||
fs.existsSync(path.join(dir, "bsconfig.json"))
) {
return dir;
} else {
if (dir === source) {
if (dir === normalizedSource) {
// reached top
return null;
} else {
Expand Down
9 changes: 5 additions & 4 deletions server/src/bsc-args/rewatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export type RewatchCompilerArgs = {

async function getRuntimePath(
entry: IncrementallyCompiledFileInfo,
): Promise<string | null> {
): Promise<utils.NormalizedPath | null> {
return utils.getRuntimePathFromWorkspaceRoot(entry.project.workspaceRootPath);
}

export async function getRewatchBscArgs(
send: (msg: p.Message) => void,
bscBinaryLocation: string | null,
projectsFiles: Map<string, projectFiles>,
bscBinaryLocation: utils.NormalizedPath | null,
projectsFiles: Map<utils.NormalizedPath, projectFiles>,
entry: IncrementallyCompiledFileInfo,
): Promise<RewatchCompilerArgs | null> {
const rewatchCacheEntry = entry.buildRewatch;
Expand Down Expand Up @@ -103,7 +103,8 @@ export async function getRewatchBscArgs(
includePrerelease: true,
})
) {
let rescriptRuntime: string | null = await getRuntimePath(entry);
let rescriptRuntime: utils.NormalizedPath | null =
await getRuntimePath(entry);

if (rescriptRuntime !== null) {
env["RESCRIPT_RUNTIME"] = rescriptRuntime;
Expand Down
11 changes: 5 additions & 6 deletions server/src/codeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
// OCaml binary.
import * as p from "vscode-languageserver-protocol";
import * as utils from "./utils";
import { fileURLToPath } from "url";

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

export type filesCodeActions = {
[key: string]: fileCodeActions[];
[key: utils.FileURI]: fileCodeActions[];
};

interface findCodeActionsConfig {
diagnostic: p.Diagnostic;
diagnosticMessage: string[];
file: string;
file: utils.FileURI;
range: p.Range;
addFoundActionsHere: filesCodeActions;
}
Expand Down Expand Up @@ -190,7 +189,7 @@ interface codeActionExtractorConfig {
line: string;
index: number;
array: string[];
file: string;
file: utils.FileURI;
range: p.Range;
diagnostic: p.Diagnostic;
codeActions: filesCodeActions;
Expand Down Expand Up @@ -327,7 +326,7 @@ let handleUndefinedRecordFieldsAction = ({
}: {
recordFieldNames: string[];
codeActions: filesCodeActions;
file: string;
file: utils.FileURI;
range: p.Range;
diagnostic: p.Diagnostic;
todoValue: string;
Expand Down Expand Up @@ -631,7 +630,7 @@ let simpleAddMissingCases: codeActionExtractor = async ({
.join("")
.trim();

let filePath = fileURLToPath(file);
let filePath = utils.uriToNormalizedPath(file);

let newSwitchCode = await utils.runAnalysisAfterSanityCheck(filePath, [
"codemod",
Expand Down
18 changes: 13 additions & 5 deletions server/src/find-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readdir, stat as statAsync, readFile } from "fs/promises";
import { join, resolve } from "path";
import { compilerInfoPartialPath } from "./constants";
import { NormalizedPath, normalizePath } from "./utils";

// Efficient parallel folder traversal to find node_modules directories
async function findNodeModulesDirs(
Expand Down Expand Up @@ -92,14 +93,18 @@ async function findRescriptRuntimeInAlternativeLayout(
return results;
}

async function findRuntimePath(project: string): Promise<string[]> {
async function findRuntimePath(
project: NormalizedPath,
): Promise<NormalizedPath[]> {
// Try a compiler-info.json file first
const compilerInfo = resolve(project, compilerInfoPartialPath);
try {
const contents = await readFile(compilerInfo, "utf8");
const compileInfo: { runtime_path?: string } = JSON.parse(contents);
if (compileInfo && compileInfo.runtime_path) {
return [compileInfo.runtime_path];
// We somewhat assume the user to pass down a normalized path, but we cannot be sure of this.
const normalizedRuntimePath = normalizePath(compileInfo.runtime_path);
return normalizedRuntimePath ? [normalizedRuntimePath] : [];
}
} catch {
// Ignore errors, fallback to node_modules search
Expand Down Expand Up @@ -146,7 +151,10 @@ async function findRuntimePath(project: string): Promise<string[]> {
}),
).then((results) => results.flatMap((x) => x));

return rescriptRuntimeDirs.map((runtime) => resolve(runtime));
return rescriptRuntimeDirs.map(
// `resolve` ensures we can assume string is now NormalizedPath
(runtime) => resolve(runtime) as NormalizedPath,
);
}

/**
Expand All @@ -156,7 +164,7 @@ async function findRuntimePath(project: string): Promise<string[]> {
* (see getRuntimePathFromWorkspaceRoot in utils.ts).
*/
export async function findRescriptRuntimesInProject(
project: string,
): Promise<string[]> {
project: NormalizedPath,
): Promise<NormalizedPath[]> {
return await findRuntimePath(project);
}
Loading