diff --git a/src/rules/no-unnormalized-keys.js b/src/rules/no-unnormalized-keys.js index 920d216..c62ba67 100644 --- a/src/rules/no-unnormalized-keys.js +++ b/src/rules/no-unnormalized-keys.js @@ -20,6 +20,24 @@ import { getKey, getRawKey } from "../util.js"; * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition */ +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Escapes a normalized string key and wraps it in its original quotes. + * @param {string} normalizedKey The normalized key to escape. + * @param {string} quote The quote character used in the original key. + * @returns {string} The escaped and quoted key. + */ +function escapeKey(normalizedKey, quote) { + const escapedKey = normalizedKey + .replaceAll("\\", "\\\\") + .replaceAll(quote, `\\${quote}`); + + return `${quote}${escapedKey}${quote}`; +} + //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- @@ -62,12 +80,13 @@ export default /** @satisfies {NoUnnormalizedKeysRuleDefinition} */ ({ }, create(context) { + const { sourceCode } = context; const [{ form }] = context.options; return /** @type {JSONRuleVisitor} */ ({ Member(node) { const key = getKey(node); - const rawKey = getRawKey(node, context.sourceCode); + const rawKey = getRawKey(node, sourceCode); const normalizedKey = key.normalize(form); if (normalizedKey !== key) { @@ -85,12 +104,15 @@ export default /** @satisfies {NoUnnormalizedKeysRuleDefinition} */ ({ return null; } - return fixer.replaceTextRange( + const fixedKey = name.type === "String" - ? [name.range[0] + 1, name.range[1] - 1] - : name.range, - normalizedKey, - ); + ? escapeKey( + normalizedKey, + sourceCode.text[name.range[0]], + ) + : normalizedKey; + + return fixer.replaceText(name, fixedKey); }, }); } diff --git a/tests/rules/no-unnormalized-keys.test.js b/tests/rules/no-unnormalized-keys.test.js index 6e12151..3d0d271 100644 --- a/tests/rules/no-unnormalized-keys.test.js +++ b/tests/rules/no-unnormalized-keys.test.js @@ -260,6 +260,209 @@ ruleTester.run("no-unnormalized-keys", rule, { }, ], }, + { + code: `{"a\uff02b": 1}`, + output: String.raw`{"a\"b": 1}`, + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{"a\uff07b": 1}`, + output: String.raw`{"a'b": 1}`, + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff07b" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{"a\uff3cb": 1}`, + output: String.raw`{"a\\b": 1}`, + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff3cb" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{'a\uff07b': 1}`, + output: String.raw`{'a\'b': 1}`, + language: "json/json5", + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff07b" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{'a\uff02b': 1}`, + output: String.raw`{'a"b': 1}`, + language: "json/json5", + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + language: "json/jsonc", + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + language: "json/json5", + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{'a\uff07b\uff3cc\uff02\uff07\uff3c': 1}`, + output: String.raw`{'a\'b\\c"\'\\': 1}`, + language: "json/json5", + options: [{ form: "NFKC" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff07b\uff3cc\uff02\uff07\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + language: "json/jsonc", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{"a\uff02b\uff3cc\uff07\uff02\uff3c": 1}`, + output: String.raw`{"a\"b\\c'\"\\": 1}`, + language: "json/json5", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff02b\uff3cc\uff07\uff02\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, + { + code: `{'a\uff07b\uff3cc\uff02\uff07\uff3c': 1}`, + output: String.raw`{'a\'b\\c"\'\\': 1}`, + language: "json/json5", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: "a\uff07b\uff3cc\uff02\uff07\uff3c" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 12, + }, + ], + }, // escaped form { code: `{"${escapedNfcO}":"NFC"}`,