Files
oxc-project__oxc/apps/oxlint/src-js/workspace/index.ts
camc314 9d4592c72f fix(linter): reject duplicate JS plugin names (#25243)
## Summary
- reject JS plugins whose normalized names collide
- keep registered plugin names isolated per LSP workspace
- add a CLI regression fixture covering two plugins that expose the same rule name

Fixes #25164
2026-08-03 09:40:27 +00:00

105 lines
3.4 KiB
TypeScript

/*
* Isolated Workspaces.
*
* Every workspace starts with a "workspace root" directory.
*
* Each workspace can be created, used, and then cleared to free up resources.
*/
import { debugAssert } from "../utils/asserts.ts";
import type { Options } from "../plugins/options.ts";
import type { RuleDetails } from "../plugins/load.ts";
/**
* Settings for a workspace.
*/
interface Workspace {
cwd: string;
rules: RuleDetails[];
pluginNames: Set<string>;
allOptions: Readonly<Options>[];
}
/**
* Active workspaces.
* Keyed by workspace URI.
*/
export const workspaces = new Map<string, Workspace>();
/**
* Most recent workspace that was used.
*/
export let currentWorkspace: Workspace | null = null;
/**
* URI of most recent workspace that was used.
*/
export let currentWorkspaceUri: string | null = null;
/**
* Create a new workspace.
*/
export function createWorkspace(workspaceUri: string): undefined {
// This assertion is commented out because we currently don't destroy workspaces.
// See comment in `destroyWorkspace` below.
// debugAssert(!workspaces.has(workspaceUri), `Workspace "${workspaceUri}" already exists`);
workspaces.set(workspaceUri, {
cwd: "",
allOptions: [],
rules: [],
pluginNames: new Set(),
});
// Set current workspace to `null` to force switching workspace in the next call to `loadPlugin`.
// Otherwise, if the new workspace has same URI as a previous workspace (which it does when reloading a workspace),
// `cwd`, `registeredRules`, `registeredPluginNames` and `allOptions` will still contain the state from the old version of the workspace.
// This means `registeredRules` does not get replaced with an empty array before loading plugins again.
// Forcing a switch to the new workspace overwrites the stale state.
currentWorkspace = null;
currentWorkspaceUri = null;
}
/**
* Destroy a workspace.
* Unloads all plugin data associated with this workspace.
*/
export function destroyWorkspace(workspaceUri: string): undefined {
// oxlint-disable no-unreachable
// We currently don't destroy workspaces.
//
// There is a race condition where sometimes LSP receives the signal to destroy a workspace *after* the signal
// to create the new one when reloading a workspace.
// Because both the old and the new workspaces have the same URI, the 2nd incarnation of the workspace would
// be added to `workspaces`, but then it'd be removed again straight away when `destroyWorkspace` is called.
//
// It'd be ideal if we did remove `Workspace` objects to free up memory, but it's not *so* important for 2 reasons:
//
// 1. Usually workspace destruction only happens when you reload workspace, so it'll get discarded within seconds
// when `createWorkspace` over-writes it anyway.
// 2. Most of the data in the workspace is persisted anyway in NodeJS module cache, so destroying workspace
// doesn't free that much memory.
return;
debugAssert(workspaces.has(workspaceUri), `Workspace "${workspaceUri}" does not exist`);
workspaces.delete(workspaceUri);
if (currentWorkspaceUri === workspaceUri) {
currentWorkspace = null;
currentWorkspaceUri = null;
}
// oxlint-enable no-unreachable
}
/**
* Set the current workspace.
* @param workspace - Workspace object
* @param workspaceUri - Workspace URI
*/
export function setCurrentWorkspace(workspace: Workspace, workspaceUri: string): void {
currentWorkspace = workspace;
currentWorkspaceUri = workspaceUri;
}