diff --git a/apps/oxlint/src-js/package/rule_tester.ts b/apps/oxlint/src-js/package/rule_tester.ts index aa8f0ec482e17..85c7fe1b0426f 100644 --- a/apps/oxlint/src-js/package/rule_tester.ts +++ b/apps/oxlint/src-js/package/rule_tester.ts @@ -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; } @@ -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; + /** * Parser options config. */ @@ -862,6 +880,7 @@ function mergeLanguageOptions( localLanguageOptions.parserOptions, baseLanguageOptions.parserOptions, ), + globals: mergeGlobals(localLanguageOptions.globals, baseLanguageOptions.globals), }; } @@ -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 @@ -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 @@ -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. * diff --git a/apps/oxlint/test/rule_tester.test.ts b/apps/oxlint/test/rule_tester.test.ts index e37385cb99bc7..d609f9d749b2b 100644 --- a/apps/oxlint/test/rule_tester.test.ts +++ b/apps/oxlint/test/rule_tester.test.ts @@ -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]); + }); + }); });