Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 1 addition & 3 deletions client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ export const createFileInTempDir = (prefix = "", extension = "") => {
return path.join(os.tmpdir(), tempFileName);
};

export let findProjectRootOfFileInDir = (
source: DocumentUri,
): null | DocumentUri => {
export let findProjectRootOfFileInDir = (source: string): null | string => {
let dir = path.dirname(source);
if (
fs.existsSync(path.join(dir, "rescript.json")) ||
Expand Down
4 changes: 2 additions & 2 deletions server/src/bsc-args/rewatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ async function getRuntimePath(

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
7 changes: 3 additions & 4 deletions server/src/codeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// 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 };

Expand All @@ -14,7 +13,7 @@ export type filesCodeActions = {
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 @@ -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
87 changes: 57 additions & 30 deletions server/src/incrementalCompilation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as path from "path";
import fs from "fs";
import * as utils from "./utils";
import { pathToFileURL } from "url";
import readline from "readline";
import { performance } from "perf_hooks";
import * as p from "vscode-languageserver-protocol";
Expand All @@ -14,7 +13,8 @@ import { fileCodeActions } from "./codeActions";
import { projectsFiles } from "./projectFiles";
import { getRewatchBscArgs, RewatchCompilerArgs } from "./bsc-args/rewatch";
import { BsbCompilerArgs, getBsbBscArgs } from "./bsc-args/bsb";
import { workspaceFolders } from "./server";
import { getCurrentCompilerDiagnosticsForFile } from "./server";
import { NormalizedPath } from "./utils";

export function debug() {
return (
Expand All @@ -32,8 +32,8 @@ export type IncrementallyCompiledFileInfo = {
file: {
/** File type. */
extension: ".res" | ".resi";
/** Path to the source file. */
sourceFilePath: string;
/** Path to the source file (normalized). */
sourceFilePath: NormalizedPath;
/** Name of the source file. */
sourceFileName: string;
/** Module name of the source file. */
Expand Down Expand Up @@ -69,29 +69,29 @@ export type IncrementallyCompiledFileInfo = {
killCompilationListeners: Array<() => void>;
/** Project specific information. */
project: {
/** The root path of the project. */
rootPath: string;
/** The root path of the project (normalized to match projectsFiles keys). */
rootPath: NormalizedPath;
/** The root path of the workspace (if a monorepo) */
workspaceRootPath: string;
workspaceRootPath: NormalizedPath;
/** Computed location of bsc. */
bscBinaryLocation: string;
bscBinaryLocation: NormalizedPath;
/** The arguments needed for bsc, derived from the project configuration/build.ninja. */
callArgs: Promise<Array<string> | null>;
/** The location of the incremental folder for this project. */
incrementalFolderPath: string;
incrementalFolderPath: NormalizedPath;
};
/** Any code actions for this incremental file. */
codeActions: Array<fileCodeActions>;
};

const incrementallyCompiledFileInfo: Map<
string,
NormalizedPath,
IncrementallyCompiledFileInfo
> = new Map();
const hasReportedFeatureFailedError: Set<string> = new Set();
const originalTypeFileToFilePath: Map<string, string> = new Map();
const originalTypeFileToFilePath: Map<string, NormalizedPath> = new Map();

export function incrementalCompilationFileChanged(changedPath: string) {
export function incrementalCompilationFileChanged(changedPath: NormalizedPath) {
const filePath = originalTypeFileToFilePath.get(changedPath);
if (filePath != null) {
const entry = incrementallyCompiledFileInfo.get(filePath);
Expand All @@ -116,7 +116,7 @@ export function incrementalCompilationFileChanged(changedPath: string) {
}

export function removeIncrementalFileFolder(
projectRootPath: string,
projectRootPath: NormalizedPath,
onAfterRemove?: () => void,
) {
fs.rm(
Expand All @@ -128,7 +128,7 @@ export function removeIncrementalFileFolder(
);
}

export function recreateIncrementalFileFolder(projectRootPath: string) {
export function recreateIncrementalFileFolder(projectRootPath: NormalizedPath) {
if (debug()) {
console.log("Recreating incremental file folder");
}
Expand All @@ -142,8 +142,8 @@ export function recreateIncrementalFileFolder(projectRootPath: string) {
}

export function cleanUpIncrementalFiles(
filePath: string,
projectRootPath: string,
filePath: NormalizedPath,
projectRootPath: NormalizedPath,
) {
const ext = filePath.endsWith(".resi") ? ".resi" : ".res";
const namespace = utils.getNamespaceNameFromConfigFile(projectRootPath);
Expand Down Expand Up @@ -242,7 +242,7 @@ function removeAnsiCodes(s: string): string {
return s.replace(ansiEscape, "");
}
function triggerIncrementalCompilationOfFile(
filePath: string,
filePath: NormalizedPath,
fileContent: string,
send: send,
onCompilationFinished?: () => void,
Expand All @@ -256,7 +256,9 @@ function triggerIncrementalCompilationOfFile(
console.log("Did not find project root path for " + filePath);
return;
}
const project = projectsFiles.get(projectRootPath);
// projectRootPath is already normalized (NormalizedPath) from findProjectRootOfFile
// Use getProjectFile to verify the project exists
const project = utils.getProjectFile(projectRootPath);
if (project == null) {
if (debug()) console.log("Did not find open project for " + filePath);
return;
Expand All @@ -266,11 +268,12 @@ function triggerIncrementalCompilationOfFile(
const computedWorkspaceRoot =
utils.computeWorkspaceRootPathFromLockfile(projectRootPath);
// If null, it means either a lockfile was found (local package) or no parent project root exists
// In both cases, we default to projectRootPath
const workspaceRootPath = computedWorkspaceRoot ?? projectRootPath;
// In both cases, we default to actualProjectRootPath
const workspaceRootPath: NormalizedPath =
computedWorkspaceRoot ?? projectRootPath;

// Determine if lockfile was found for debug logging
// If computedWorkspaceRoot is null and projectRootPath is not null, check if parent exists
// If computedWorkspaceRoot is null and actualProjectRootPath is not null, check if parent exists
const foundRewatchLockfileInProjectRoot =
computedWorkspaceRoot == null &&
projectRootPath != null &&
Expand Down Expand Up @@ -299,10 +302,10 @@ function triggerIncrementalCompilationOfFile(
? `${moduleName}-${project.namespaceName}`
: moduleName;

const incrementalFolderPath = path.join(
const incrementalFolderPath: NormalizedPath = path.join(
projectRootPath,
INCREMENTAL_FILE_FOLDER_LOCATION,
);
) as NormalizedPath;
Comment on lines +306 to +309
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct type assertion to NormalizedPath bypasses the normalization logic. Use normalizePath(path.join(...)) instead to ensure the path is actually normalized. The pattern should be: normalizePath(path.join(projectRootPath, INCREMENTAL_FILE_FOLDER_LOCATION))! or handle the null case if needed.

Copilot uses AI. Check for mistakes.

let originalTypeFileLocation = path.resolve(
projectRootPath,
Expand All @@ -326,7 +329,7 @@ function triggerIncrementalCompilationOfFile(
incrementalFilePath: path.join(incrementalFolderPath, moduleName + ext),
},
project: {
workspaceRootPath: workspaceRootPath ?? projectRootPath,
workspaceRootPath,
rootPath: projectRootPath,
callArgs: Promise.resolve([]),
bscBinaryLocation,
Expand Down Expand Up @@ -373,7 +376,10 @@ function triggerIncrementalCompilationOfFile(
};
}
}
function verifyTriggerToken(filePath: string, triggerToken: number): boolean {
function verifyTriggerToken(
filePath: NormalizedPath,
triggerToken: number,
): boolean {
return (
incrementallyCompiledFileInfo.get(filePath)?.compilation?.triggerToken ===
triggerToken
Expand Down Expand Up @@ -578,7 +584,7 @@ async function compileContents(
const change = Object.values(ca.codeAction.edit.changes)[0];

ca.codeAction.edit.changes = {
[pathToFileURL(entry.file.sourceFilePath).toString()]: change,
[utils.pathToURI(entry.file.sourceFilePath)]: change,
};
}
});
Expand Down Expand Up @@ -645,11 +651,32 @@ async function compileContents(
}
}

const fileUri = utils.pathToURI(entry.file.sourceFilePath);

// Update filesWithDiagnostics to track this file
// entry.project.rootPath is guaranteed to match a key in projectsFiles
// (see triggerIncrementalCompilationOfFile where the entry is created)
const projectFile = projectsFiles.get(entry.project.rootPath);

if (projectFile != null) {
// Get compiler diagnostics from main build (if any)
const compilerDiagnosticsForFile =
getCurrentCompilerDiagnosticsForFile(fileUri);
const allDiagnostics = [...res, ...compilerDiagnosticsForFile];

if (allDiagnostics.length > 0) {
projectFile.filesWithDiagnostics.add(fileUri);
} else {
// Only remove if there are no diagnostics at all
projectFile.filesWithDiagnostics.delete(fileUri);
}
}

const notification: p.NotificationMessage = {
jsonrpc: c.jsonrpcVersion,
method: "textDocument/publishDiagnostics",
params: {
uri: pathToFileURL(entry.file.sourceFilePath),
uri: fileUri,
diagnostics: res,
},
};
Expand All @@ -667,7 +694,7 @@ async function compileContents(
}

export function handleUpdateOpenedFile(
filePath: string,
filePath: utils.NormalizedPath,
fileContent: string,
send: send,
onCompilationFinished?: () => void,
Expand All @@ -683,7 +710,7 @@ export function handleUpdateOpenedFile(
);
}

export function handleClosedFile(filePath: string) {
export function handleClosedFile(filePath: NormalizedPath) {
if (debug()) {
console.log("Closed: " + filePath);
}
Expand All @@ -695,7 +722,7 @@ export function handleClosedFile(filePath: string) {
}

export function getCodeActionsFromIncrementalCompilation(
filePath: string,
filePath: NormalizedPath,
): Array<fileCodeActions> | null {
const entry = incrementallyCompiledFileInfo.get(filePath);
if (entry != null) {
Expand Down
Loading