From ea8ec6db3dd9f3147186e3300d6842d3e51363d2 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 24 Jul 2025 11:33:57 -0400 Subject: [PATCH 1/3] fix: Ensure languageOptions.customSyntax is serializable fixes #211 --- src/languages/css-language.js | 78 +++++++++++++++++++++++++- tests/languages/css-language.test.js | 60 ++++++++++++++++++++ tests/plugin/eslint.test.js | 83 ++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/src/languages/css-language.js b/src/languages/css-language.js index ab1a784a..51a7301d 100644 --- a/src/languages/css-language.js +++ b/src/languages/css-language.js @@ -51,6 +51,34 @@ const blockCloserTokenTypes = new Map([ [tokenTypes.RightSquareBracket, "["], ]); +/** + * Recursively replaces all function values in an object with boolean true. + * Used to make objects serializable for JSON output. + * + * @param {Record} object The object to process. + * @returns {Record} A copy of the object with all functions replaced by true. + */ +function replaceFunctions(object) { + if (typeof object !== "object" || object === null) { + return object; + } + if (Array.isArray(object)) { + return object.map(replaceFunctions); + } + const result = {}; + for (const key of Object.keys(object)) { + const value = object[key]; + if (typeof value === "function") { + result[key] = true; + } else if (typeof value === "object" && value !== null) { + result[key] = replaceFunctions(value); + } else { + result[key] = value; + } + } + return result; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -101,7 +129,8 @@ export class CSSLanguage { /** * Validates the language options. * @param {CSSLanguageOptions} languageOptions The language options to validate. - * @throws {Error} When the language options are invalid. + * @returns {void} + * @throws {TypeError} When the language options are invalid. */ validateLanguageOptions(languageOptions) { if ( @@ -125,6 +154,53 @@ export class CSSLanguage { } } + /** + * Normalizes the language options to they can be serialized. + * @param {CSSLanguageOptions} languageOptions The language options to normalize. + * @returns {CSSLanguageOptions} The normalized language options. + */ + normalizeLanguageOptions(languageOptions) { + // if there's no custom syntax then no changes are necessary + if (!languageOptions?.customSyntax) { + return languageOptions; + } + + Object.defineProperty(languageOptions, "toJSON", { + value() { + // Shallow copy + const result = { ...languageOptions }; + + if (result.customSyntax) { + result.customSyntax = { ...result.customSyntax }; + + if (result.customSyntax.node) { + result.customSyntax.node = replaceFunctions( + result.customSyntax.node, + ); + } + + if (result.customSyntax.scope) { + result.customSyntax.scope = replaceFunctions( + result.customSyntax.scope, + ); + } + + if (result.customSyntax.atrule) { + result.customSyntax.atrule = replaceFunctions( + result.customSyntax.atrule, + ); + } + } + + return result; + }, + enumerable: false, + configurable: true, + }); + + return languageOptions; + } + /** * Parses the given file into an AST. * @param {File} file The virtual file to parse. diff --git a/tests/languages/css-language.test.js b/tests/languages/css-language.test.js index a346644d..c54eefe6 100644 --- a/tests/languages/css-language.test.js +++ b/tests/languages/css-language.test.js @@ -261,4 +261,64 @@ describe("CSSLanguage", () => { assert.strictEqual(sourceCode.comments.length, 1); }); }); + + describe("normalizeLanguageOptions", () => { + it("should return the same object if no customSyntax is present", () => { + const language = new CSSLanguage(); + const options = { tolerant: true }; + const result = language.normalizeLanguageOptions(options); + assert.strictEqual(result, options); + assert.strictEqual(typeof result.toJSON, "undefined"); + }); + + it("should add a toJSON method if customSyntax is present", () => { + const language = new CSSLanguage(); + const options = { tolerant: true, customSyntax: { foo: "bar" } }; + const result = language.normalizeLanguageOptions(options); + assert.strictEqual(result, options); + assert.strictEqual(typeof result.toJSON, "function"); + }); + + it("should replace functions with true in toJSON output", () => { + const language = new CSSLanguage(); + const options = { + tolerant: false, + customSyntax: { + node: { + foo() {}, + bar: 42, + baz: { + qux() {}, + }, + }, + scope: { + test() {}, + }, + atrule: { + other() {}, + }, + }, + }; + language.normalizeLanguageOptions(options); + const json = options.toJSON(); + assert.deepStrictEqual(json, { + tolerant: false, + customSyntax: { + node: { + foo: true, + bar: 42, + baz: { + qux: true, + }, + }, + scope: { + test: true, + }, + atrule: { + other: true, + }, + }, + }); + }); + }); }); diff --git a/tests/plugin/eslint.test.js b/tests/plugin/eslint.test.js index 02ae8690..19bdc01f 100644 --- a/tests/plugin/eslint.test.js +++ b/tests/plugin/eslint.test.js @@ -101,6 +101,89 @@ describe("Plugin", () => { assert.strictEqual(results[0].messages[1].column, 18); }); }); + + describe("serialization", () => { + it("should serialize the config to JSON", async () => { + const languageOptions = { + tolerant: true, + }; + + const configWithLanguageOptions = { + ...config, + languageOptions, + }; + + const eslint = new ESLint({ + overrideConfigFile: true, + overrideConfig: configWithLanguageOptions, + }); + + const resultConfig = + await eslint.calculateConfigForFile("test.css"); + const serializedConfig = JSON.stringify( + resultConfig.languageOptions, + null, + 2, + ); + const expectedConfig = JSON.stringify(languageOptions, null, 2); + assert.strictEqual(serializedConfig, expectedConfig); + }); + + it("should serialize the config to JSON when it has functions", async () => { + const languageOptions = { + tolerant: true, + customSyntax: { + node: { + CustomNode: { + parse() {}, + }, + }, + scope: { + Value: { + theme() {}, + }, + }, + atrule: { + CustomAtRule: { + parse: { + prelude() {}, + }, + }, + }, + }, + }; + + const configWithLanguageOptions = { + ...config, + languageOptions, + }; + + const eslint = new ESLint({ + overrideConfigFile: true, + overrideConfig: configWithLanguageOptions, + }); + + const resultConfig = + await eslint.calculateConfigForFile("test.css"); + const serializedConfig = JSON.stringify( + resultConfig.languageOptions, + null, + 2, + ); + const expectedConfig = JSON.stringify( + languageOptions, + (key, value) => { + if (typeof value === "function") { + return true; + } + + return value; + }, + 2, + ); + assert.strictEqual(serializedConfig, expectedConfig); + }); + }); }); describe("Configuration Comments", () => { From 781e71980066236358ba266e13c533c3c5ebfb1a Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 29 Jul 2025 12:04:50 -0400 Subject: [PATCH 2/3] Update src/languages/css-language.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/languages/css-language.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/css-language.js b/src/languages/css-language.js index 51a7301d..c8ad5bc8 100644 --- a/src/languages/css-language.js +++ b/src/languages/css-language.js @@ -155,7 +155,7 @@ export class CSSLanguage { } /** - * Normalizes the language options to they can be serialized. + * Normalizes the language options so they can be serialized. * @param {CSSLanguageOptions} languageOptions The language options to normalize. * @returns {CSSLanguageOptions} The normalized language options. */ From b42acc4377f13db3480a9552701fed5b21051025 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 29 Jul 2025 12:09:23 -0400 Subject: [PATCH 3/3] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/languages/css-language.js | 37 ++++++++++++++++------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/languages/css-language.js b/src/languages/css-language.js index c8ad5bc8..9f368f69 100644 --- a/src/languages/css-language.js +++ b/src/languages/css-language.js @@ -169,27 +169,24 @@ export class CSSLanguage { value() { // Shallow copy const result = { ...languageOptions }; + result.customSyntax = { ...result.customSyntax }; - if (result.customSyntax) { - result.customSyntax = { ...result.customSyntax }; - - if (result.customSyntax.node) { - result.customSyntax.node = replaceFunctions( - result.customSyntax.node, - ); - } - - if (result.customSyntax.scope) { - result.customSyntax.scope = replaceFunctions( - result.customSyntax.scope, - ); - } - - if (result.customSyntax.atrule) { - result.customSyntax.atrule = replaceFunctions( - result.customSyntax.atrule, - ); - } + if (result.customSyntax.node) { + result.customSyntax.node = replaceFunctions( + result.customSyntax.node, + ); + } + + if (result.customSyntax.scope) { + result.customSyntax.scope = replaceFunctions( + result.customSyntax.scope, + ); + } + + if (result.customSyntax.atrule) { + result.customSyntax.atrule = replaceFunctions( + result.customSyntax.atrule, + ); } return result;