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
1 change: 1 addition & 0 deletions apps/oxlint/src-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type * as ESTree from "./generated/types.d.ts";
// Plugin types
export type { Context, LanguageOptions } from "./plugins/context.ts";
export type { Fix, Fixer, FixFn } from "./plugins/fix.ts";
export type { Globals, Envs } from "./plugins/globals.ts";
export type { CreateOnceRule, CreateRule, Plugin, Rule } from "./plugins/load.ts";
export type { Options, RuleOptionsSchema } from "./plugins/options.ts";
export type { Diagnostic, DiagnosticData, Suggestion } from "./plugins/report.ts";
Expand Down
56 changes: 43 additions & 13 deletions apps/oxlint/src-js/package/rule_tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ interface Config {
interface LanguageOptions {
sourceType?: SourceType;
globals?: Globals;
env?: Envs;
parserOptions?: ParserOptions;
}

Expand Down Expand Up @@ -165,6 +166,11 @@ type GlobalValue =
*/
type Globals = Record<string, GlobalValue>;

/**
* Environments for the file being linted.
*/
export type Envs = Record<string, boolean>;

/**
* Parser options config.
*/
Expand Down Expand Up @@ -1074,25 +1080,24 @@ function getParseOptions(test: TestCase): ParseOptions {
}

/**
* Get globals as JSON for test case.
*
* Normalizes values to "readonly", "writable", or "off", same as Rust side does.
* Get globals and envs as JSON for test case.
*
* Normalizes globals values to "readonly", "writable", or "off", same as Rust side does.
* `null` is only supported in ESLint compatibility mode.
*
* Removes envs which are false, same as Rust side does.
*
* @param test - Test case
* @returns Globals as JSON string
* @returns Globals and envs as JSON string of form `{ "globals": { ... }, "envs": { ... } }`
*/
function getGlobalsJson(test: TestCase): string {
const globals = test.languageOptions?.globals;
if (globals == null) return "{}";

// Normalize values to `readonly`, `writable`, or `off` - same as Rust side does
const cloned = { ...globals },
// Get globals.
// Normalize values to `readonly`, `writable`, or `off` - same as Rust side does.
const globals = { ...test.languageOptions?.globals },
eslintCompat = !!test.eslintCompat;

for (const key in cloned) {
let value = cloned[key];
for (const key in globals) {
let value = globals[key];

switch (value) {
case "readonly":
Expand Down Expand Up @@ -1127,10 +1132,31 @@ function getGlobalsJson(test: TestCase): string {
);
}

cloned[key] = value;
globals[key] = value;
}

// TODO: Tests for `env` in `RuleTester` tests

// Get envs.
// Remove properties which are `false` - same as Rust side does.
const originalEnvs = test.languageOptions?.env;
const envs: Envs = {};
if (originalEnvs != null) {
for (const [key, value] of Object.entries(originalEnvs)) {
if (value === false) continue;

// Use `Object.defineProperty` to handle if `key` is "__proto__"
Object.defineProperty(envs, key, {
value: true,
writable: true,
enumerable: true,
configurable: true,
});
}
}

return JSON.stringify(cloned);
// Serialize globals + envs to JSON
return JSON.stringify({ globals, envs });
}

/**
Expand Down Expand Up @@ -1434,6 +1460,8 @@ function isSerializablePrimitiveOrPlainObject(value: unknown): boolean {
// Add types to `RuleTester` namespace
type _Config = Config;
type _LanguageOptions = LanguageOptions;
type _Globals = Globals;
type _Envs = Envs;
type _ParserOptions = ParserOptions;
type _SourceType = SourceType;
type _Language = Language;
Expand All @@ -1448,6 +1476,8 @@ type _Error = Error;
export namespace RuleTester {
export type Config = _Config;
export type LanguageOptions = _LanguageOptions;
export type Globals = _Globals;
export type Envs = _Envs;
export type ParserOptions = _ParserOptions;
export type SourceType = _SourceType;
export type Language = _Language;
Expand Down
13 changes: 12 additions & 1 deletion apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ import { report } from "./report.ts";
import { settings, initSettings } from "./settings.ts";
import visitorKeys from "../generated/keys.ts";
import { debugAssertIsNonNull } from "../utils/asserts.ts";
import { Globals, globals, initGlobals } from "./globals.ts";
import { envs, globals, initGlobals } from "./globals.ts";

import type { Globals, Envs } from "./globals.ts";
import type { RuleDetails } from "./load.ts";
import type { Options } from "./options.ts";
import type { Diagnostic } from "./report.ts";
Expand Down Expand Up @@ -190,6 +191,16 @@ const LANGUAGE_OPTIONS = {
debugAssertIsNonNull(globals);
return globals;
},

/**
* Environments defined for the file being linted.
*/
get env(): Readonly<Envs> {
// This is a property which ESLint does not have - it uses `ecmaVersion` instead for preset environments
if (envs === null) initGlobals();
debugAssertIsNonNull(envs);
return envs;
},
};

// In conformance build, replace `LANGUAGE_OPTIONS.ecmaVersion` with a getter which returns value of local var.
Expand Down
36 changes: 19 additions & 17 deletions apps/oxlint/src-js/plugins/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts";
*/
export type Globals = Record<string, "readonly" | "writable" | "off">;

// Empty globals object.
// When globals are empty, we use this singleton object to avoid allocating a new object each time.
const EMPTY_GLOBALS: Globals = Object.freeze({});
/**
* Environments for the file being linted.
*
* Only includes environments that are enabled, so all properties are `true`.
*/
export type Envs = Record<string, true>;

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

/**
* Updates the globals for the file.
Expand All @@ -42,28 +46,26 @@ export function setGlobalsForFile(globalsJSONInput: string): undefined {
export function initGlobals(): void {
debugAssertIsNonNull(globalsJSON);

if (globalsJSON === "{}") {
// Re-use a single object for empty globals as an optimization
globals = EMPTY_GLOBALS;
} else {
globals = JSON.parse(globalsJSON);

// 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);
}
({ globals, envs } = JSON.parse(globalsJSON));

debugAssertIsNonNull(globals);
debugAssert(
typeof globals === "object" && !Array.isArray(globals),
typeof globals === "object" && globals !== null && !Array.isArray(globals),
"`globals` should be an object",
);
debugAssert(
typeof envs === "object" && envs !== null && !Array.isArray(envs),
"`envs` should be an object",
);

Object.freeze(globals);
Object.freeze(envs);
}

/**
* Reset globals.
*/
export function resetGlobals(): undefined {
globals = null;
envs = null;
globalsJSON = null;
}
1 change: 1 addition & 0 deletions apps/oxlint/src-js/utils/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const {
assign: ObjectAssign,
getPrototypeOf: ObjectGetPrototypeOf,
setPrototypeOf: ObjectSetPrototypeOf,
entries: ObjectEntries,
} = Object;

export const { prototype: ArrayPrototype, isArray: ArrayIsArray, from: ArrayFrom } = Array;
Expand Down
14 changes: 13 additions & 1 deletion apps/oxlint/test/fixtures/globals/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,23 @@
},
"overrides": [
{
"files": ["files/nested/**"],
"files": ["files/nested/*.js"],
"env": {
"browser": true,
"node": true
}
},
{
"files": ["files/nested/2.js"],
"globals": {
"React": "writable",
"process": "off",
"customGlobal": "readonly"
},
"env": {
"browser": false,
"astro": true,
"chai": false
}
}
]
Expand Down
1 change: 1 addition & 0 deletions apps/oxlint/test/fixtures/globals/files/nested/2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let y;
43 changes: 37 additions & 6 deletions apps/oxlint/test/fixtures/globals/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

# stdout
```
x globals-plugin(globals): {
x globals-plugin(globals):
| globals: {
| "React": "readonly",
| "console": "readonly",
| "baz": "writable",
Expand All @@ -12,13 +13,38 @@
| "bar": "readonly",
| "qux": "readonly",
| "window": "off"
| }
| env: {
| "builtin": true
| }
,-[files/index.js:1:1]
1 | debugger;
: ^
`----

x globals-plugin(globals): {
x globals-plugin(globals):
| globals: {
| "React": "readonly",
| "console": "readonly",
| "baz": "writable",
| "foo": "writable",
| "process": "writable",
| "bar": "readonly",
| "qux": "readonly",
| "window": "off"
| }
| env: {
| "browser": true,
| "node": true,
| "builtin": true
| }
,-[files/nested/1.js:1:1]
1 | let x;
: ^
`----

x globals-plugin(globals):
| globals: {
| "React": "writable",
| "console": "readonly",
| "baz": "writable",
Expand All @@ -29,13 +55,18 @@
| "customGlobal": "readonly",
| "window": "off"
| }
,-[files/nested/index.js:1:1]
1 | let x;
| env: {
| "astro": true,
| "builtin": true,
| "node": true
| }
,-[files/nested/2.js:1:1]
1 | let y;
: ^
`----

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

# stderr
Expand Down
5 changes: 4 additions & 1 deletion apps/oxlint/test/fixtures/globals/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const plugin: Plugin = {
rules: {
globals: {
create(context) {
const { languageOptions } = context;
context.report({
message: JSON.stringify(context.languageOptions.globals, null, 2),
message:
`\nglobals: ${JSON.stringify(languageOptions.globals, null, 2)}\n` +
`env: ${JSON.stringify(languageOptions.env, null, 2)}`,
node: SPAN,
});
return {};
Expand Down
3 changes: 3 additions & 0 deletions apps/oxlint/test/fixtures/languageOptions/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
| ecmaVersion: 2026
| parserOptions: {"sourceType":"script"}
| globals: {}
| env: {"builtin":true}
,-[files/index.cjs:1:1]
1 | let x;
: ^
Expand Down Expand Up @@ -795,6 +796,7 @@
| ecmaVersion: 2026
| parserOptions: {"sourceType":"module"}
| globals: {}
| env: {"builtin":true}
,-[files/index.js:1:1]
1 | let x;
: ^
Expand All @@ -805,6 +807,7 @@
| ecmaVersion: 2026
| parserOptions: {"sourceType":"module"}
| globals: {}
| env: {"builtin":true}
,-[files/index.mjs:1:1]
1 | let x;
: ^
Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/test/fixtures/languageOptions/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const plugin: Plugin = {
`sourceType: ${languageOptions.sourceType}\n` +
`ecmaVersion: ${languageOptions.ecmaVersion}\n` +
`parserOptions: ${JSON.stringify(languageOptions.parserOptions)}\n` +
`globals: ${JSON.stringify(languageOptions.globals)}`,
`globals: ${JSON.stringify(languageOptions.globals)}\n` +
`env: ${JSON.stringify(languageOptions.env)}`,
node: SPAN,
});

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, OxlintGlobals, OxlintSettings},
config::{LintConfig, LintPlugins, OxlintEnv, OxlintGlobals, OxlintSettings},
disable_directives::{DisableDirectives, DisableDirectivesBuilder, RuleCommentType},
fixer::{Fix, FixKind, Message, PossibleFixes},
frameworks::{self, FrameworkOptions},
Expand Down Expand Up @@ -264,6 +264,11 @@ impl<'a> ContextHost<'a> {
&self.config.globals
}

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

/// 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