Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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);
}
14 changes: 9 additions & 5 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 @@ -184,11 +185,14 @@ 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);

return globals === EMPTY_GLOBALS ? null : globals;
},
});

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

import { debugAssertIsNonNull } from "../utils/asserts.js";

/**
* 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.
*/
export function initGlobals(): void {
debugAssertIsNonNull(globalsJSON);

if (globalsJSON === "{}") {
globals = EMPTY_GLOBALS;
return;
}

globals = JSON.parse(globalsJSON);

// `globals` was deserialized from JSON, so we can use a simple `for..in` loop here
for (const key in globals) {
// @ts-expect-error globals is not made immutable yet
if (globals[key] === "writeable") 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.
Object.freeze(globals);
}

/**
* Reset globals.
*/
export function resetGlobals(): undefined {
globals = null;
globalsJSON = null;
}
12 changes: 11 additions & 1 deletion 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.js";

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 @@ -142,6 +148,9 @@ export function lintFileImpl(
// Pass settings JSON to settings module
setSettingsForFile(settingsJSON);

// Pass globals JSON to globals module
setGlobalsForFile(globalsJSON);

// Get visitors for this file from all rules
initCompiledVisitor();

Expand Down Expand Up @@ -233,4 +242,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
9 changes: 8 additions & 1 deletion crates/oxc_linter/src/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ pub type ExternalLinterLoadPluginCb =
pub type ExternalLinterSetupConfigsCb = Box<dyn Fn(String) -> Result<(), String> + Send + Sync>;

pub type ExternalLinterLintFileCb = Box<
dyn Fn(String, Vec<u32>, Vec<u32>, String, &Allocator) -> Result<Vec<LintFileResult>, String>
dyn Fn(
String,
Vec<u32>,
Vec<u32>,
String,
String,
&Allocator,
) -> Result<Vec<LintFileResult>, String>
+ Sync
+ Send,
>;
Expand Down
Loading
Loading