diff --git a/apps/oxlint/conformance/snapshots/stylistic.md b/apps/oxlint/conformance/snapshots/stylistic.md index fd168072f73ef..3bc731057ea24 100644 --- a/apps/oxlint/conformance/snapshots/stylistic.md +++ b/apps/oxlint/conformance/snapshots/stylistic.md @@ -231,10 +231,10 @@ Skip: 0 / 217 (0.0%) ``` Error: Overlapping token/comments: last end: 75, next start: 24 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) #### jsx-tag-spacing > valid @@ -292,10 +292,10 @@ Error: Overlapping token/comments: last end: 75, next start: 24 ``` Error: Overlapping token/comments: last end: 88, next start: 24 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) #### jsx-tag-spacing > invalid @@ -332,10 +332,10 @@ Error: Overlapping token/comments: last end: 88, next start: 24 ``` Error: Overlapping token/comments: last end: 81, next start: 30 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) #### jsx-tag-spacing > invalid @@ -399,10 +399,10 @@ Error: Overlapping token/comments: last end: 81, next start: 30 ``` Error: Overlapping token/comments: last end: 94, next start: 30 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) #### jsx-tag-spacing > invalid @@ -442,10 +442,10 @@ Error: Overlapping token/comments: last end: 94, next start: 30 ``` Error: Overlapping token/comments: last end: 105, next start: 54 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) #### jsx-tag-spacing > invalid @@ -512,8 +512,8 @@ Error: Overlapping token/comments: last end: 105, next start: 54 ``` Error: Overlapping token/comments: last end: 118, next start: 54 - at debugCheckValidRanges (apps/oxlint/dist/load.js) - at debugCheckTokensAndComments (apps/oxlint/dist/load.js) - at initTokensAndComments (apps/oxlint/dist/load.js) - at Object.isSpaceBetween (apps/oxlint/dist/load.js) + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) diff --git a/apps/oxlint/src-js/package/rule_tester.ts b/apps/oxlint/src-js/package/rule_tester.ts index 1591dfcae85b1..a73ea68c9db45 100644 --- a/apps/oxlint/src-js/package/rule_tester.ts +++ b/apps/oxlint/src-js/package/rule_tester.ts @@ -12,13 +12,12 @@ import { join as pathJoin, isAbsolute as isAbsolutePath, dirname } from "node:pa import util from "node:util"; import stableJsonStringify from "json-stable-stringify-without-jsonify"; import { ecmaFeaturesOverride, setEcmaVersion, ECMA_VERSION } from "../plugins/context.ts"; -import { registerPlugin } from "../plugins/load.ts"; +import { registerPlugin, registeredRules } from "../plugins/load.ts"; import { lintFileImpl, resetStateAfterError } from "../plugins/lint.ts"; import { getLineColumnFromOffset, getNodeByRangeIndex } from "../plugins/location.ts"; -import { setOptions, DEFAULT_OPTIONS_ID } from "../plugins/options.ts"; +import { allOptions, setOptions, DEFAULT_OPTIONS_ID } from "../plugins/options.ts"; import { diagnostics, replacePlaceholders, PLACEHOLDER_REGEX } from "../plugins/report.ts"; import { parse } from "./parse.ts"; -import { createWorkspace, destroyWorkspace } from "../workspace/index.ts"; import type { RequireAtLeastOne } from "type-fest"; import type { Plugin, Rule } from "../plugins/load.ts"; @@ -358,11 +357,6 @@ const DEFAULT_FILENAME_BASE = "file"; // Root of `oxlint` package once bundled into `dist`. const DEFAULT_CWD = dirname(import.meta.dirname); -// Dummy workspace URI. -// This just needs to be unique, and we only have a single workspace in existence at any time. -// It can be anything, so just use empty string. -const WORKSPACE_URI = ""; - // ------------------------------------------------------------------------------ // `RuleTester` class // ------------------------------------------------------------------------------ @@ -1019,8 +1013,6 @@ function lint(test: TestCase, plugin: Plugin): Diagnostic[] { path = pathJoin(cwd, filename); } - createWorkspace(WORKSPACE_URI); - try { // Register plugin. This adds rule to `registeredRules` array. registerPlugin(plugin, null, false, null); @@ -1081,7 +1073,8 @@ function lint(test: TestCase, plugin: Plugin): Diagnostic[] { }); } finally { // Reset state - destroyWorkspace(WORKSPACE_URI); + registeredRules.length = 0; + if (allOptions !== null) allOptions.length = 1; // Even if there hasn't been an error, do a full reset of state just to be sure. // This includes emptying `diagnostics`. diff --git a/apps/oxlint/src-js/plugins/context.ts b/apps/oxlint/src-js/plugins/context.ts index 198ef90a6c0f2..62a5c41be135c 100644 --- a/apps/oxlint/src-js/plugins/context.ts +++ b/apps/oxlint/src-js/plugins/context.ts @@ -46,17 +46,23 @@ import type { ModuleKind, Program } from "../generated/types.d.ts"; export let filePath: string | null = null; // Current working directory for file being linted. -// When `null`, indicates that no file is currently being linted (in `createOnce`, or between linting files). -let cwd: string | null = null; +// Set by `setOptions` at end of registering all plugins, and may also be changed when switching workspaces. +export let cwd: string | null = null; + +/** + * Set CWD. Used when switching workspaces. + * @param cwdInput - CWD + */ +export function setCwd(cwdInput: string) { + cwd = cwdInput; +} /** * Set up context for linting a file. * @param filePathInput - Absolute path of file being linted - * @param cwdInput - Current working directory for file being linted */ -export function setupFileContext(filePathInput: string, cwdInput: string): void { +export function setupFileContext(filePathInput: string): void { filePath = filePathInput; - cwd = cwdInput; } /** @@ -68,7 +74,6 @@ export function setupFileContext(filePathInput: string, cwdInput: string): void */ export function resetFileContext(): void { filePath = null; - cwd = null; } // ECMAScript version. This matches ESLint's default. @@ -367,7 +372,8 @@ const FILE_CONTEXT = Object.freeze({ */ get cwd(): string { // Note: If we change this implementation, also change `getCwd` method below - if (cwd === null) throw new Error("Cannot access `context.cwd` in `createOnce`"); + if (filePath === null) throw new Error("Cannot access `context.cwd` in `createOnce`"); + debugAssertIsNonNull(cwd, "`cwd` should not be null"); return cwd; }, @@ -377,7 +383,8 @@ const FILE_CONTEXT = Object.freeze({ * @deprecated Use `context.cwd` property instead. */ getCwd(): string { - if (cwd === null) throw new Error("Cannot call `context.getCwd` in `createOnce`"); + if (filePath === null) throw new Error("Cannot call `context.getCwd` in `createOnce`"); + debugAssertIsNonNull(cwd, "`cwd` should not be null"); return cwd; }, diff --git a/apps/oxlint/src-js/plugins/lint.ts b/apps/oxlint/src-js/plugins/lint.ts index 55a7053f7f72d..0168a430e9ed0 100644 --- a/apps/oxlint/src-js/plugins/lint.ts +++ b/apps/oxlint/src-js/plugins/lint.ts @@ -1,7 +1,7 @@ import { walkProgramWithCfg, resetCfgWalk } from "./cfg.ts"; import { setupFileContext, resetFileContext } from "./context.ts"; import { registeredRules } from "./load.ts"; -import { allOptions, cwds, DEFAULT_OPTIONS_ID } from "./options.ts"; +import { allOptions, DEFAULT_OPTIONS_ID } from "./options.ts"; import { diagnostics } from "./report.ts"; import { setSettingsForFile, resetSettings } from "./settings.ts"; import { ast, initAst, resetSourceAndAst, setupSourceForFile } from "./source_code.ts"; @@ -9,7 +9,7 @@ import { HAS_BOM_FLAG_POS } from "../generated/constants.ts"; import { typeAssertIs, debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; import { getErrorMessage } from "../utils/utils.ts"; import { setGlobalsForFile, resetGlobals } from "./globals.ts"; -import { getCliWorkspace } from "../workspace/index.ts"; +import { switchWorkspace } from "./workspace.ts"; import { addVisitorToCompiled, compiledVisitor, @@ -154,22 +154,14 @@ export function lintFileImpl( "`ruleIds` and `optionsIds` should be same length", ); - // Get workspace containing file. - // In CLI (`workspaceUri` is `null`), use the single workspace (the CWD passed to `setupRuleConfigs`). - // In LSP, use the provided workspace URI. - if (workspaceUri === null) workspaceUri = getCliWorkspace(); + // Switch to requested workspace. + // In CLI, `workspaceUri` is `null`, and there's only 1 workspace, so no need to switch. + // In LSP, there can be multiple workspaces, so we need to switch if we're not already in the right one. + if (workspaceUri !== null) switchWorkspace(workspaceUri); + debugAssertIsNonNull(allOptions, "`allOptions` should be initialized"); - const cwd = cwds.get(workspaceUri); - debugAssertIsNonNull(cwd, `No CWD registered for workspace "${workspaceUri}"`); - - // Pass file path and CWD to context module, so `Context`s know what file is being linted - setupFileContext(filePath, cwd); - - // Load all relevant workspace configurations - const rules = registeredRules.get(workspaceUri)!; - const workspaceOptions = allOptions.get(workspaceUri); - debugAssertIsNonNull(rules, "No rules registered for workspace"); - debugAssertIsNonNull(workspaceOptions, "No options registered for workspace"); + // Pass file path to context module, so `Context`s know what file is being linted + setupFileContext(filePath); // Pass buffer to source code module, so it can decode source text and deserialize AST on demand. // @@ -191,20 +183,20 @@ export function lintFileImpl( for (let i = 0, len = ruleIds.length; i < len; i++) { const ruleId = ruleIds[i]; - debugAssert(ruleId < rules.length, "Rule ID out of bounds"); - const ruleDetails = rules[ruleId]; + debugAssert(ruleId < registeredRules.length, "Rule ID out of bounds"); + const ruleDetails = registeredRules[ruleId]; // Set `ruleIndex` for rule. It's used when sending diagnostics back to Rust. ruleDetails.ruleIndex = i; // Set `options` for rule const optionsId = optionsIds[i]; - debugAssert(optionsId < workspaceOptions.length, "Options ID out of bounds"); + debugAssert(optionsId < allOptions.length, "Options ID out of bounds"); // If the rule has no user-provided options, use the plugin-provided default // options (which falls back to `DEFAULT_OPTIONS`) ruleDetails.options = - optionsId === DEFAULT_OPTIONS_ID ? ruleDetails.defaultOptions : workspaceOptions[optionsId]; + optionsId === DEFAULT_OPTIONS_ID ? ruleDetails.defaultOptions : allOptions[optionsId]; let { visitor } = ruleDetails; if (visitor === null) { diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index 3997a7bd1e4ca..0d155871979fd 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -1,9 +1,9 @@ -import { getCliWorkspace } from "../workspace/index.ts"; import { createContext } from "./context.ts"; import { deepFreezeJsonArray } from "./json.ts"; import { compileSchema, DEFAULT_OPTIONS } from "./options.ts"; +import { switchWorkspace } from "./workspace.ts"; import { getErrorMessage } from "../utils/utils.ts"; -import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; +import { debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Writable } from "type-fest"; import type { Context } from "./context.ts"; @@ -82,7 +82,16 @@ interface CreateOnceRuleDetails extends RuleDetailsBase { // Rule objects for loaded rules. // Indexed by `ruleId`, which is passed to `lintFile`. -export const registeredRules: Map = new Map(); +// May be changed when switching workspaces. +export let registeredRules: RuleDetails[] = []; + +/** + * Set `registeredRules`. Used when switching workspaces. + * @param rules - Array of `RuleDetails` objects + */ +export function setRegisteredRules(rules: RuleDetails[]) { + registeredRules = rules; +} // `before` hook which makes rule never run. const neverRunBeforeHook: BeforeHook = () => false; @@ -145,14 +154,12 @@ export function registerPlugin( pluginName = getPluginName(plugin, pluginName, pluginNameIsAlias); - // In CLI mode (`workspaceUri` is `null`), use the CWD from `setupRuleConfigs` (stored as CLI_WORKSPACE). - // In LSP mode, use the provided workspace URI. - if (workspaceUri === null) workspaceUri = getCliWorkspace(); - - const registeredRulesForWorkspace = registeredRules.get(workspaceUri); - debugAssertIsNonNull(registeredRulesForWorkspace, "Workspace must have registered rules array"); + // Switch to requested workspace. + // In CLI, `workspaceUri` is `null`, and there's only 1 workspace, so no need to switch. + // In LSP, there can be multiple workspaces, so we need to switch if we're not already in the right one. + if (workspaceUri !== null) switchWorkspace(workspaceUri); - const offset = registeredRulesForWorkspace.length ?? 0; + const offset = registeredRules.length; const { rules } = plugin; const ruleNames = Object.keys(rules); const ruleNamesLen = ruleNames.length; @@ -277,7 +284,7 @@ export function registerPlugin( (ruleDetails as unknown as Writable).afterHook = afterHook; } - registeredRulesForWorkspace.push(ruleDetails); + registeredRules.push(ruleDetails); } return { name: pluginName, offset, ruleNames }; @@ -431,18 +438,3 @@ function conformHookFn(hookFn: H | null | undefined, hookName: string): H | n } return hookFn; } - -export function setupPluginSystemForWorkspace(workspace: string) { - debugAssert( - !registeredRules.has(workspace), - "Workspace must not already have registered rules array", - ); - registeredRules.set(workspace, []); -} - -/** - * Remove all plugins and rules associated with a workspace. - */ -export function removePluginsInWorkspace(workspace: string) { - registeredRules.delete(workspace); -} diff --git a/apps/oxlint/src-js/plugins/options.ts b/apps/oxlint/src-js/plugins/options.ts index 465a7c86d9de7..07ebfcc8d9c59 100644 --- a/apps/oxlint/src-js/plugins/options.ts +++ b/apps/oxlint/src-js/plugins/options.ts @@ -6,10 +6,11 @@ import assert from "node:assert"; import Ajv from "ajv"; import ajvPackageJson from "ajv/package.json" with { type: "json" }; import metaSchema from "ajv/lib/refs/json-schema-draft-04.json" with { type: "json" }; +import { setCwd } from "./context.ts"; import { registeredRules } from "./load.ts"; import { deepCloneJsonValue, deepFreezeJsonArray } from "./json.ts"; +import { currentWorkspace, switchWorkspace } from "./workspace.ts"; import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; -import { getCliWorkspace } from "../workspace/index.ts"; import type { JSONSchema4 } from "json-schema"; import type { Writable } from "type-fest"; @@ -50,10 +51,16 @@ export const DEFAULT_OPTIONS: Readonly = Object.freeze([]); // All rule options. // `lintFile` is called with an array of options IDs, which are indices into this array. // First element is irrelevant - never accessed - because 0 index is a sentinel meaning default options. -export const allOptions: Map[]> = new Map(); +// Set by `setOptions` at end of registering all plugins, and may also be changed when switching workspaces. +export let allOptions: Readonly[] | null = null; -// Mapping from workspace URIs to CWD paths -export const cwds: Map = new Map(); +/** + * Set `allOptions`. Used when switching workspaces. + * @param options - Array of options objects + */ +export function setAllOptions(options: Readonly[]) { + allOptions = options; +} // Index into `allOptions` for default options export const DEFAULT_OPTIONS_ID = 0; @@ -184,25 +191,24 @@ function wrapSchemaValidator(validate: Ajv.ValidateFunction): SchemaValidator { */ export function setOptions(optionsJson: string): void { const details = JSON.parse(optionsJson); + allOptions = details.options; + debugAssertIsNonNull(allOptions, "`options` included in `optionsJSON` should not be null"); - let { workspaceUri } = details; - if (workspaceUri === null) workspaceUri = getCliWorkspace(); - - const { ruleIds, cwd, options } = details; + const { ruleIds, cwd } = details; // Validate if (DEBUG) { assert(typeof cwd === "string", `cwd must be a string, got ${typeof cwd}`); - assert(Array.isArray(options), `options must be an array, got ${typeof options}`); + assert(Array.isArray(allOptions), `options must be an array, got ${typeof allOptions}`); assert(Array.isArray(ruleIds), `ruleIds must be an array, got ${typeof ruleIds}`); assert.strictEqual( - options.length, + allOptions.length, ruleIds.length, "ruleIds and options arrays must be the same length", ); - for (const option of options) { - assert(Array.isArray(option), `Elements of options must be arrays, got ${typeof option}`); + for (const options of allOptions) { + assert(Array.isArray(options), `Elements of options must be arrays, got ${typeof options}`); } for (const ruleId of ruleIds) { @@ -213,28 +219,33 @@ export function setOptions(optionsJson: string): void { } } - allOptions.set(workspaceUri, options); + // Switch to requested workspace. + // In CLI, `workspaceUri` is `null`, and there's only 1 workspace, so no need to switch. + // In LSP, there can be multiple workspaces, so we need to switch if we're not already in the right one. + const { workspaceUri } = details; + if (workspaceUri !== null) switchWorkspace(workspaceUri); - debugAssert(!cwds.has(workspaceUri), "Workspace must not already have registered CWD"); - cwds.set(workspaceUri, cwd); + // Set CWD + setCwd(cwd); // Process each options array. // For each options array, merge with default options and apply schema defaults for the corresponding rule. // Skip the first, as index 0 is a sentinel value meaning default options. First element is never accessed. // `processOptions` also deep-freezes the options. - const registeredWorkspaceRules = registeredRules.get(workspaceUri); - debugAssertIsNonNull( - registeredWorkspaceRules, - `No registered rules for workspace "${workspaceUri}"`, - ); - - for (let i = 1, len = options.length; i < len; i++) { - options[i] = processOptions( + for (let i = 1, len = allOptions.length; i < len; i++) { + allOptions[i] = processOptions( // `allOptions`' type is `Readonly`, but the array is mutable at present - options[i] as Writable<(typeof options)[number]>, - registeredWorkspaceRules[ruleIds[i]], + allOptions[i] as Writable<(typeof allOptions)[number]>, + registeredRules[ruleIds[i]], ); } + + // Store `cwd` and `allOptions` in workspace + if (workspaceUri !== null) { + debugAssertIsNonNull(currentWorkspace, "`currentWorkspace` should be initialized"); + currentWorkspace.cwd = cwd; + currentWorkspace.allOptions = allOptions; + } } /** @@ -385,24 +396,3 @@ function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue return configValue; } - -/** - * Setups all needed data structures to hold options for a workspace. - * - * @param workspace the workspace identifier - */ -export function setupOptionsForWorkspace(workspace: string) { - debugAssert( - !allOptions.has(workspace), - "Workspace must not already have registered options array", - ); - allOptions.set(workspace, []); -} - -/** - * Remove all options associated with a workspace. - */ -export function removeOptionsInWorkspace(workspace: string) { - allOptions.delete(workspace); - cwds.delete(workspace); -} diff --git a/apps/oxlint/src-js/plugins/workspace.ts b/apps/oxlint/src-js/plugins/workspace.ts new file mode 100644 index 0000000000000..6acb72aab573e --- /dev/null +++ b/apps/oxlint/src-js/plugins/workspace.ts @@ -0,0 +1,33 @@ +import { setCwd } from "./context.ts"; +import { setRegisteredRules } from "./load.ts"; +import { setAllOptions } from "./options.ts"; +import { + workspaces, + currentWorkspace, + currentWorkspaceUri, + setCurrentWorkspace, +} from "../workspace/index.ts"; +import { debugAssertIsNonNull } from "../utils/asserts.ts"; + +export { currentWorkspace }; + +export function switchWorkspace(workspaceUri: string): void { + // If requested workspace is already the current one, nothing to do. + // + // This is a fast path for common cases: + // 1. Only a single workspace exists (most common). + // 2. When user does have multiple workspaces, but works in one workspace for a lengthy period. + if (currentWorkspaceUri === workspaceUri) return; + + // Get workspace + const workspace = workspaces.get(workspaceUri); + debugAssertIsNonNull(workspace, `Workspace "${workspaceUri}" does not exist`); + + // Change global state to that of the workspace + setCwd(workspace.cwd); + setRegisteredRules(workspace.rules); + setAllOptions(workspace.allOptions); + + // Set this workspace as the current one + setCurrentWorkspace(workspace, workspaceUri); +} diff --git a/apps/oxlint/src-js/workspace/index.ts b/apps/oxlint/src-js/workspace/index.ts index 34d327e2d4a51..56732cb30d75a 100644 --- a/apps/oxlint/src-js/workspace/index.ts +++ b/apps/oxlint/src-js/workspace/index.ts @@ -1,52 +1,79 @@ /* * Isolated Workspaces. * - * Every Workspace starts with a "workspace root" directory. This directory is - * used to isolate the plugin's dependencies from other plugins and the main - * application. + * Every workspace starts with a "workspace root" directory. * * Each workspace can be created, used, and then cleared to free up resources. */ -import { removePluginsInWorkspace, setupPluginSystemForWorkspace } from "../plugins/load"; -import { removeOptionsInWorkspace, setupOptionsForWorkspace } from "../plugins/options"; -import { debugAssert } from "../utils/asserts"; +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[]; + allOptions: Readonly[]; +} + +/** + * Active workspaces. + * Keyed by workspace URI. + */ +export const workspaces = new Map(); /** - * Set of workspace IDs. + * Most recent workspace that was used. */ -const workspaces = new Set(); +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(workspace: string): undefined { - debugAssert(!workspaces.has(workspace), `Workspace "${workspace.toString()}" already exists`); - workspaces.add(workspace); - setupPluginSystemForWorkspace(workspace); - setupOptionsForWorkspace(workspace); +export function createWorkspace(workspaceUri: string): undefined { + debugAssert(!workspaces.has(workspaceUri), `Workspace "${workspaceUri}" already exists`); + + const workspace = { + cwd: "", + allOptions: [], + rules: [], + }; + + workspaces.set(workspaceUri, workspace); + currentWorkspace = workspace; + currentWorkspaceUri = workspaceUri; } /** * Destroy a workspace. * Unloads all plugin data associated with this workspace. */ -export function destroyWorkspace(workspace: string): undefined { - debugAssert(workspaces.has(workspace), `Workspace "${workspace.toString()}" does not exist`); +export function destroyWorkspace(workspaceUri: string): undefined { + debugAssert(workspaces.has(workspaceUri), `Workspace "${workspaceUri}" does not exist`); + + workspaces.delete(workspaceUri); - workspaces.delete(workspace); - removePluginsInWorkspace(workspace); - removeOptionsInWorkspace(workspace); + if (currentWorkspaceUri === workspaceUri) { + currentWorkspace = null; + currentWorkspaceUri = null; + } } /** - * Gets the CLI workspace ID. - * In CLI mode, there is exactly one workspace (the CWD), so this returns that workspace ID. - */ -export function getCliWorkspace(): string { - debugAssert( - workspaces.size === 1, - "getCliWorkspace should only be used in CLI mode with 1 workspace", - ); - return workspaces.values().next().value; + * Set the current workspace. + * @param workspace - Workspace object + * @param workspaceUri - Workspace URI + */ +export function setCurrentWorkspace(workspace: Workspace, workspaceUri: string): void { + currentWorkspace = workspace; + currentWorkspaceUri = workspaceUri; } diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 571be18da095b..0d99ee9e03a3f 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -190,19 +190,6 @@ impl CliRunner { let base_ignore_patterns = oxlintrc.ignore_patterns.clone(); - // Setup JS workspace before loading any configs (config parsing can load JS plugins). - if let Some(external_linter) = &external_linter { - // Workspace URI doesn't need to be a valid URI, it just needs to be unique. - // In CLI we only have a single workspace in existence at any time, so it can be anything. - // So just use empty string. - let res = (external_linter.create_workspace)(String::new()); - - if let Err(err) = res { - print_and_flush_stdout(stdout, &format!("Failed to setup JS workspace:\n{err}\n")); - return CliRunResult::JsPluginWorkspaceSetupFailed; - } - } - let config_builder = match ConfigStoreBuilder::from_oxlintrc( false, oxlintrc.clone(), diff --git a/apps/oxlint/test/isSpaceBetween.test.ts b/apps/oxlint/test/isSpaceBetween.test.ts index b540fe773684f..802420ca67e9c 100644 --- a/apps/oxlint/test/isSpaceBetween.test.ts +++ b/apps/oxlint/test/isSpaceBetween.test.ts @@ -25,10 +25,9 @@ import type { Program } from "../src-js/generated/types.d.ts"; * @returns AST */ function parse(filename: string, sourceText: string, options?: ParseOptions): Program { - // Set file path and CWD - const cwd = import.meta.dirname; - const path = pathJoin(cwd, filename); - setupFileContext(path, cwd); + // Set file path + const path = pathJoin(import.meta.dirname, filename); + setupFileContext(path); // Parse source, writing source text and AST into buffer parseRaw(path, sourceText, options); diff --git a/apps/oxlint/test/tokens.test.ts b/apps/oxlint/test/tokens.test.ts index 142213e40950f..9c54415849fa4 100644 --- a/apps/oxlint/test/tokens.test.ts +++ b/apps/oxlint/test/tokens.test.ts @@ -54,11 +54,10 @@ function setup(sourceText: string) { resetFileContext(); resetSourceAndAst(); - // Set file path and CWD + // Set file path const filename = "dummy.js"; - const cwd = import.meta.dirname; - const path = pathJoin(cwd, filename); - setupFileContext(path, cwd); + const path = pathJoin(import.meta.dirname, filename); + setupFileContext(path); // Parse source text into buffer parseRaw(path, sourceText);