diff --git a/src/rules/no-unnormalized-keys.js b/src/rules/no-unnormalized-keys.js index 920d216..eab9138 100644 --- a/src/rules/no-unnormalized-keys.js +++ b/src/rules/no-unnormalized-keys.js @@ -20,6 +20,22 @@ import { getKey, getRawKey } from "../util.js"; * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition */ +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Escapes a normalized string key for use inside 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 key. + */ +function escapeKey(normalizedKey, quote) { + return normalizedKey + .replaceAll("\\", "\\\\") + .replaceAll(quote, `\\${quote}`); +} + //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- @@ -62,19 +78,20 @@ 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) { - const { name } = node; + const { loc, range, type } = node.name; context.report({ - loc: name.loc, + loc, messageId: "unnormalizedKey", data: { key: rawKey, @@ -86,10 +103,15 @@ export default /** @satisfies {NoUnnormalizedKeysRuleDefinition} */ ({ } return fixer.replaceTextRange( - name.type === "String" - ? [name.range[0] + 1, name.range[1] - 1] - : name.range, - normalizedKey, + type === "String" + ? [range[0] + 1, range[1] - 1] + : range, + type === "String" + ? escapeKey( + normalizedKey, + sourceCode.text[range[0]], + ) + : normalizedKey, ); }, }); 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"}`,