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
108 changes: 102 additions & 6 deletions apps/oxlint/src-js/package/rule_tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,7 @@ interface Config {
*/
interface LanguageOptions {
sourceType?: SourceType;
globals?: Record<
string,
boolean | "true" | "writable" | "writeable" | "false" | "readonly" | "readable" | "off" | null
>;
globals?: Globals;
parserOptions?: ParserOptions;
}

Expand All @@ -147,6 +144,27 @@ interface LanguageOptionsInternal extends LanguageOptions {
*/
type SourceType = "script" | "module" | "unambiguous" | "commonjs";

/**
* Value of a property in `globals` object.
*
* Note: `null` only supported in ESLint compatibility mode.
*/
type GlobalValue =
| boolean
| "true"
| "writable"
| "writeable"
| "false"
| "readonly"
| "readable"
| "off"
| null;

/**
* Globals object.
*/
type Globals = Record<string, GlobalValue>;

/**
* Parser options config.
*/
Expand Down Expand Up @@ -862,6 +880,7 @@ function mergeLanguageOptions(
localLanguageOptions.parserOptions,
baseLanguageOptions.parserOptions,
),
globals: mergeGlobals(localLanguageOptions.globals, baseLanguageOptions.globals),
};
}

Expand Down Expand Up @@ -903,6 +922,21 @@ function mergeEcmaFeatures(
return { ...baseEcmaFeatures, ...localEcmaFeatures };
}

/**
* Merge globals from test case / config onto globals from base config.
* @param localGlobals - Globals from test case / config
* @param baseGlobals - Globals from base config
* @returns Merged globals
*/
function mergeGlobals(
localGlobals?: Globals | null,
baseGlobals?: Globals | null,
): Globals | undefined {
if (localGlobals == null) return baseGlobals ?? undefined;
if (baseGlobals == null) return localGlobals;
return { ...baseGlobals, ...localGlobals };
}

/**
* Lint a test case.
* @param test - Test case
Expand Down Expand Up @@ -940,10 +974,12 @@ function lint(test: TestCase, plugin: Plugin): Diagnostic[] {
// This is not supported outside of conformance tests.
if (CONFORMANCE) setEcmaVersionContext(test);

// Get globals and settings
const globalsJSON: string = getGlobalsJson(test);
const settingsJSON = "{}"; // TODO

// Lint file.
// Buffer is stored already, at index 0. No need to pass it.
const settingsJSON = "{}"; // TODO
const globalsJSON = "{}"; // TODO
lintFileImpl(filename, 0, null, [0], [optionsId], settingsJSON, globalsJSON);

// Return diagnostics
Expand Down Expand Up @@ -1035,6 +1071,66 @@ function getParseOptions(test: TestCase): ParseOptions {
return parseOptions;
}

/**
* Get globals as JSON for test case.
*
* Normalizes values to "readonly", "writable", or "off", same as Rust side does.
*
* `null` is only supported in ESLint compatibility mode.
*
* @param test - Test case
* @returns Globals as JSON string
*/
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 },
eslintCompat = !!test.eslintCompat;

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

switch (value) {
case "readonly":
case "writable":
case "off":
continue;

case "writeable":
case "true":
case true:
value = "writable";
break;

case "readable":
case "false":
case false:
value = "readonly";
break;

// ESLint treats `null` as `readonly` (undocumented).
// https://github.com/eslint/eslint/blob/ba71baa87265888b582f314163df1d727441e2f1/lib/languages/js/source-code/source-code.js#L119-L149
// But Oxlint (Rust code) doesn't support it, so we don't support it here either unless in ESLint compatibility mode.
case null:
if (eslintCompat) {
value = "readonly";
break;
}

default:
throw new Error(
`'${value}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`,
);
}

cloned[key] = value;
}

return JSON.stringify(cloned);
}

/**
* Set up options for the test case.
*
Expand Down
222 changes: 222 additions & 0 deletions apps/oxlint/test/rule_tester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2812,4 +2812,226 @@ describe("RuleTester", () => {
});
});
});

describe("globals", () => {
const globalReporterRule: Rule = {
create(context) {
return {
Program(node) {
context.report({
message: `globals: ${JSON.stringify(context.languageOptions.globals)}`,
node,
});
},
};
},
};

it("is empty object if no globals defined", () => {
const tester = new RuleTester();
tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
errors: [
{
message: "globals: {}",
},
],
},
],
});
expect(runCases()).toEqual([null]);
});

describe("set", () => {
it("globally", () => {
RuleTester.setDefaultConfig({
languageOptions: {
globals: {
read: "readonly",
write: "writable",
disabled: "off",
},
},
});

const tester = new RuleTester();
tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
errors: [
{
message: 'globals: {"read":"readonly","write":"writable","disabled":"off"}',
},
],
},
],
});
expect(runCases()).toEqual([null]);
});

it("in `RuleTester` options", () => {
const tester = new RuleTester({
languageOptions: {
globals: {
read: "readonly",
write: "writable",
disabled: "off",
},
},
});

tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
errors: [
{
message: 'globals: {"read":"readonly","write":"writable","disabled":"off"}',
},
],
},
],
});
expect(runCases()).toEqual([null]);
});

it("in test case", () => {
const tester = new RuleTester();
tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
languageOptions: {
globals: {},
},
errors: [
{
message: "globals: {}",
},
],
},
{
code: "",
languageOptions: {
globals: {
read: "readonly",
write: "writable",
disabled: "off",
},
},
errors: [
{
message: 'globals: {"read":"readonly","write":"writable","disabled":"off"}',
},
],
},
],
});
expect(runCases()).toEqual([null, null]);
});
});

it("merged between global config, config, and test case", () => {
RuleTester.setDefaultConfig({
languageOptions: {
globals: {
globalConfig: "readonly",
globalConfigOverriddenByConfig: "readonly",
globalConfigOverriddenByTestCase: "readonly",
globalConfigOverriddenByBoth: "readonly",
},
},
});

const tester = new RuleTester({
languageOptions: {
globals: {
config: "writable",
globalConfigOverriddenByConfig: "writable",
globalConfigOverriddenByBoth: "writable",
configOverriddenByTestCase: "writable",
},
},
});

tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
languageOptions: {
globals: {
testCase: "off",
globalConfigOverriddenByTestCase: "off",
globalConfigOverriddenByBoth: "off",
configOverriddenByTestCase: "off",
},
},
errors: [
{
message: `globals: ${JSON.stringify({
globalConfig: "readonly",
globalConfigOverriddenByConfig: "writable",
globalConfigOverriddenByTestCase: "off",
globalConfigOverriddenByBoth: "off",
config: "writable",
configOverriddenByTestCase: "off",
testCase: "off",
})}`,
},
],
},
],
});
expect(runCases()).toEqual([null]);
});

it("normalizes values", () => {
const tester = new RuleTester();
tester.run("no-foo", globalReporterRule, {
valid: [],
invalid: [
{
code: "",
languageOptions: {
globals: {
writable: "writable",
writeable: "writeable",
true: true,
trueStr: "true",
readonly: "readonly",
readable: "readable",
false: false,
falseStr: "false",
off: "off",
},
},
errors: [
{
message: `globals: ${JSON.stringify({
writable: "writable",
writeable: "writable",
true: "writable",
trueStr: "writable",
readonly: "readonly",
readable: "readonly",
false: "readonly",
falseStr: "readonly",
off: "off",
})}`,
},
],
},
],
});
expect(runCases()).toEqual([null]);
});
});
});
Loading