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
2 changes: 1 addition & 1 deletion apps/oxlint/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export declare function getBufferOffset(buffer: Uint8Array): number

/** JS callback to lint a file. */
export type JsLintFileCb =
((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array<number>, arg4: Array<number>, arg5: string) => string | null)
((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array<number>, arg4: Array<number>, arg5: string, arg6: string) => string | null)

/** JS callback to load a JS plugin. */
export type JsLoadPluginCb =
Expand Down
4 changes: 3 additions & 1 deletion apps/oxlint/src-js/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function setupConfigsWrapper(optionsJSON: string): void {
* @param ruleIds - IDs of rules to run on this file
* @param optionsIds - IDs of options to use for rules on this file, in same order as `ruleIds`
* @param settingsJSON - Settings for file, as JSON
* @param globalsJSON - Globals for file, as JSON
* @returns Diagnostics or error serialized to JSON string
*/
function lintFileWrapper(
Expand All @@ -62,11 +63,12 @@ function lintFileWrapper(
ruleIds: number[],
optionsIds: number[],
settingsJSON: string,
globalsJSON: string,
): string | null {
// `lintFileWrapper` is never called without `loadPluginWrapper` being called first,
// so `lintFile` must be defined here
debugAssertIsNonNull(lintFile);
return lintFile(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON);
return lintFile(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON, globalsJSON);
}

// Get command line arguments, skipping first 2 (node binary and script path)
Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/src-js/package/rule_tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,8 @@ function lint(test: TestCase, plugin: Plugin, config: Config): Diagnostic[] {
// Lint file.
// Buffer is stored already, at index 0. No need to pass it.
const settingsJSON = "{}"; // TODO
lintFileImpl(path, 0, null, [0], [optionsId], settingsJSON);
const globalsJSON = "{}"; // TODO
lintFileImpl(path, 0, null, [0], [optionsId], settingsJSON, globalsJSON);

// Return diagnostics
const ruleId = `${plugin.meta!.name!}/${Object.keys(plugin.rules)[0]}`;
Expand Down
2 changes: 1 addition & 1 deletion apps/oxlint/src-js/plugins/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import { setOptions } from "./options.ts";
* @param optionsJSON - Array of all rule options across all configurations, serialized as JSON
*/
export function setupConfigs(optionsJSON: string): void {
// TODO: setup settings using this function
// TODO: setup settings and globals using this function
setOptions(optionsJSON);
}
17 changes: 11 additions & 6 deletions apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { report } from "./report.ts";
import { settings, initSettings } from "./settings.ts";
import visitorKeys from "../generated/keys.ts";
import { debugAssertIsNonNull } from "../utils/asserts.ts";
import { EMPTY_GLOBALS, Globals, globals, initGlobals } from "./globals.ts";

import type { RuleDetails } from "./load.ts";
import type { Options } from "./options.ts";
Expand Down Expand Up @@ -183,12 +184,16 @@ const LANGUAGE_OPTIONS = freeze({
/**
* Globals defined for the file being linted.
*/
// ESLint has `globals` as `null`, not empty object, if no globals are defined.
get globals(): Record<string, "readonly" | "writable" | "off"> | null {
// TODO: Get globals from Rust side.
// Note: ESLint's type is "writable", whereas Oxlint's is "writeable" (misspelled with extra "e").
// Probably we should fix that on Rust side (while still allowing "writeable").
return null;
get globals(): Readonly<Globals> | null {
if (filePath === null) {
throw new Error("Cannot access `context.languageOptions.globals` in `createOnce`");
}

if (globals === null) initGlobals();
debugAssertIsNonNull(globals);

// ESLint has `globals` as `null`, not empty object, if no globals are defined
return globals === EMPTY_GLOBALS ? null : globals;
},
});

Expand Down
78 changes: 78 additions & 0 deletions apps/oxlint/src-js/plugins/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Methods related to globals.
*/

import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts";

import { Writable } from "type-fest";

const { freeze } = Object;

/**
* Globals for the file being linted.
*
* Globals are deserialized from JSON, so can only contain JSON-compatible values.
* Each global variable maps to "readonly", "writable", or "off".
*/
export type Globals = Record<string, "readonly" | "writable" | "off">;

// Empty globals object.
// No need to freeze this object, as it's never passed to user.
export const EMPTY_GLOBALS: Globals = {};

// Globals for current file.
// `globalsJSON` is set before linting a file by `setGlobalsForFile`.
// `globals` is deserialized from `globalsJSON` lazily upon first access.
let globalsJSON: string | null = null;
export let globals: Readonly<Globals> | null = null;

/**
* Updates the globals for the file.
*
* TODO(perf): Globals are deserialized once per file to accommodate folder level settings,
* even if the globals haven't changed.
*
* @param globalsJSONInput - Globals for the file as JSON
*/
export function setGlobalsForFile(globalsJSONInput: string): undefined {
globalsJSON = globalsJSONInput;
}

/**
* Deserialize globals from JSON.
*
* Caller must ensure that `globalsJSON` has been initialized before calling this function.
*/
export function initGlobals(): void {
debugAssertIsNonNull(globalsJSON);

// `EMPTY_GLOBALS` is a placeholder meaning "no globals defined".
// `globals` getter on `LanguageOptions` returns `null` if `globals === EMPTY_GLOBALS`.
if (globalsJSON === "{}") {
globals = EMPTY_GLOBALS;
return;
}

globals = JSON.parse(globalsJSON);
debugAssert(globals !== null && typeof globals === "object");

// `globals` was deserialized from JSON, so we can use a simple `for..in` loop here
for (const key in globals) {
if ((globals[key] as string) === "writeable") {
// `globals` is not frozen yet
(globals as Writable<typeof globals>)[key] = "writable";
}
}

// Freeze the globals object, to prevent any mutation of `globals` by plugins.
// No need to deep freeze since all keys are just strings.
freeze(globals);
}

/**
* Reset globals.
*/
export function resetGlobals(): undefined {
globals = null;
globalsJSON = null;
}
12 changes: 10 additions & 2 deletions apps/oxlint/src-js/plugins/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { setSettingsForFile, resetSettings } from "./settings.ts";
import { ast, initAst, resetSourceAndAst, setupSourceForFile } from "./source_code.ts";
import { typeAssertIs, debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts";
import { getErrorMessage } from "../utils/utils.ts";
import { setGlobalsForFile, resetGlobals } from "./globals.ts";

import {
addVisitorToCompiled,
compiledVisitor,
Expand Down Expand Up @@ -48,6 +50,7 @@ const PARSER_SERVICES_DEFAULT: Record<string, unknown> = Object.freeze({});
* @param ruleIds - IDs of rules to run on this file
* @param optionsIds - IDs of options to use for rules on this file, in same order as `ruleIds`
* @param settingsJSON - Settings for this file, as JSON string
* @param globalsJSON - Globals for this file, as JSON string
* @returns Diagnostics or error serialized to JSON string
*/
export function lintFile(
Expand All @@ -57,9 +60,10 @@ export function lintFile(
ruleIds: number[],
optionsIds: number[],
settingsJSON: string,
globalsJSON: string,
): string | null {
try {
lintFileImpl(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON);
lintFileImpl(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON, globalsJSON);

// Avoid JSON serialization in common case that there are no diagnostics to report
if (diagnostics.length === 0) return null;
Expand All @@ -84,6 +88,7 @@ export function lintFile(
* @param ruleIds - IDs of rules to run on this file
* @param optionsIds - IDs of options to use for rules on this file, in same order as `ruleIds`
* @param settingsJSON - Settings for this file, as JSON string
* @param globalsJSON - Globals for this file, as JSON string
* @throws {Error} If any parameters are invalid
* @throws {*} If any rule throws
*/
Expand All @@ -94,6 +99,7 @@ export function lintFileImpl(
ruleIds: number[],
optionsIds: number[],
settingsJSON: string,
globalsJSON: string,
) {
// If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
// Do this before checks below, to make sure buffer doesn't get garbage collected when not expected
Expand Down Expand Up @@ -139,8 +145,9 @@ export function lintFileImpl(
const parserServices = PARSER_SERVICES_DEFAULT; // TODO: Set this correctly
setupSourceForFile(buffer, hasBOM, parserServices);

// Pass settings JSON to settings module
// Pass settings and globals JSON to modules that handle them
setSettingsForFile(settingsJSON);
setGlobalsForFile(globalsJSON);

// Get visitors for this file from all rules
initCompiledVisitor();
Expand Down Expand Up @@ -233,4 +240,5 @@ export function resetFile() {
resetFileContext();
resetSourceAndAst();
resetSettings();
resetGlobals();
}
11 changes: 10 additions & 1 deletion apps/oxlint/src/js_plugins/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {
rule_ids: Vec<u32>,
options_ids: Vec<u32>,
settings_json: String,
globals_json: String,
allocator: &Allocator| {
let (tx, rx) = channel();

Expand All @@ -147,7 +148,15 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {

// Send data to JS
let status = cb.call_with_return_value(
FnArgs::from((file_path, buffer_id, buffer, rule_ids, options_ids, settings_json)),
FnArgs::from((
file_path,
buffer_id,
buffer,
rule_ids,
options_ids,
settings_json,
globals_json,
)),
ThreadsafeFunctionCallMode::NonBlocking,
move |result, _env| {
// This call cannot fail, because `rx.recv()` below blocks until it receives a message.
Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ pub type JsLintFileCb = ThreadsafeFunction<
Vec<u32>, // Array of rule IDs
Vec<u32>, // Array of options IDs
String, // Settings for the file, as JSON string
String, // Globals for the file, as JSON string
)>,
// Return value
Option<String>, // `Vec<LintFileResult>`, serialized to JSON, or `None` if no diagnostics
// Arguments (repeated)
FnArgs<(String, u32, Option<Uint8Array>, Vec<u32>, Vec<u32>, String)>,
FnArgs<(String, u32, Option<Uint8Array>, Vec<u32>, Vec<u32>, String, String)>,
// Error status
Status,
// CalleeHandled
Expand Down
29 changes: 29 additions & 0 deletions apps/oxlint/test/fixtures/globals/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"jsPlugins": [
"./plugin.ts"
],
"categories": {
"correctness": "off"
},
"rules": {
"globals-plugin/globals": "error"
},
"globals": {
"React": "readonly",
"process": "writable",
"console": "readonly",
"window": "off"
},
"overrides": [
{
"files": [
"files/nested/**"
],
"globals": {
"React": "writable",
"process": "off",
"customGlobal": "readonly"
}
}
]
}
1 change: 1 addition & 0 deletions apps/oxlint/test/fixtures/globals/files/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debugger;
1 change: 1 addition & 0 deletions apps/oxlint/test/fixtures/globals/files/nested/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let x;
37 changes: 37 additions & 0 deletions apps/oxlint/test/fixtures/globals/output.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Exit code
1

# stdout
```
x globals-plugin(globals): {
| "React": "readonly",
| "console": "readonly",
| "process": "writable",
| "window": "off"
| }
,-[files/index.js:1:1]
1 | debugger;
: ^
`----

x globals-plugin(globals): {
| "React": "writable",
| "console": "readonly",
| "customGlobal": "readonly",
| "process": "off",
| "window": "off"
| }
,-[files/nested/index.js:1:1]
1 | let x;
: ^
`----

Found 0 warnings and 2 errors.
Finished in Xms on 2 files using X threads.
```

# stderr
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
30 changes: 30 additions & 0 deletions apps/oxlint/test/fixtures/globals/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Node, Plugin } from "#oxlint";

const SPAN: Node = {
start: 0,
end: 0,
range: [0, 0],
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
},
};

const plugin: Plugin = {
meta: {
name: "globals-plugin",
},
rules: {
globals: {
create(context) {
context.report({
message: JSON.stringify(context.languageOptions.globals, null, 2),
node: SPAN,
});
return {};
},
},
},
};

export default plugin;
1 change: 1 addition & 0 deletions crates/oxc_linter/src/config/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl OxlintGlobals {
#[serde(rename_all = "lowercase")]
pub enum GlobalValue {
Readonly,
// TODO: #[serde(rename = "writable")] for ESLint compatibility
Writeable,
Off,
}
Expand Down
7 changes: 6 additions & 1 deletion crates/oxc_linter/src/context/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use oxc_span::{SourceType, Span};

use crate::{
AllowWarnDeny, FrameworkFlags,
config::{LintConfig, LintPlugins, OxlintSettings},
config::{LintConfig, LintPlugins, OxlintGlobals, OxlintSettings},
disable_directives::{DisableDirectives, DisableDirectivesBuilder, RuleCommentType},
fixer::{Fix, FixKind, Message, PossibleFixes},
frameworks::{self, FrameworkOptions},
Expand Down Expand Up @@ -259,6 +259,11 @@ impl<'a> ContextHost<'a> {
&self.config.settings
}

#[inline]
pub fn globals(&self) -> &OxlintGlobals {
&self.config.globals
}

/// Add a diagnostic message to the end of the list of diagnostics. Can be used
/// by any rule to report issues.
#[inline]
Expand Down
Loading
Loading