diff --git a/src/rules/no-unnormalized-keys.js b/src/rules/no-unnormalized-keys.js index 8794cff..052f017 100644 --- a/src/rules/no-unnormalized-keys.js +++ b/src/rules/no-unnormalized-keys.js @@ -24,6 +24,8 @@ const rule = { meta: { type: "problem", + fixable: "code", + docs: { recommended: true, description: "Disallow JSON keys that are not normalized", @@ -57,19 +59,25 @@ const rule = { const [{ form }] = context.options; return { - Member(node) { - const key = - node.name.type === "String" - ? node.name.value - : node.name.name; + Member({ name }) { + const key = name.type === "String" ? name.value : name.name; + const normalizedKey = key.normalize(form); - if (key.normalize(form) !== key) { + if (normalizedKey !== key) { context.report({ - loc: node.name.loc, + loc: name.loc, messageId: "unnormalizedKey", data: { key, }, + fix(fixer) { + return fixer.replaceTextRange( + name.type === "String" + ? [name.range[0] + 1, name.range[1] - 1] + : name.range, + normalizedKey, + ); + }, }); } }, diff --git a/tests/rules/no-unnormalized-keys.test.js b/tests/rules/no-unnormalized-keys.test.js index 6b1bde5..a4f5cc6 100644 --- a/tests/rules/no-unnormalized-keys.test.js +++ b/tests/rules/no-unnormalized-keys.test.js @@ -23,6 +23,10 @@ const ruleTester = new RuleTester({ }); const o = "\u1E9B\u0323"; +const escapedNfcO = "\\u1E9B\\u0323"; +const escapedNfdO = "\\u017F\\u0323\\u0307"; +const escapedNfkcO = "\\u1E69"; +const escapedNfkdO = "\\u0073\\u0323\\u0307"; ruleTester.run("no-unnormalized-keys", rule, { valid: [ @@ -43,10 +47,29 @@ ruleTester.run("no-unnormalized-keys", rule, { code: `{"${o.normalize("NFKD")}":"NFKD"}`, options: [{ form: "NFKD" }], }, + // escaped form + `{"${escapedNfcO}":"NFC"}`, + { + code: `{"${escapedNfcO}":"NFC"}`, + options: [{ form: "NFC" }], + }, + { + code: `{"${escapedNfdO}":"NFD"}`, + options: [{ form: "NFD" }], + }, + { + code: `{"${escapedNfkcO}":"NFKC"}`, + options: [{ form: "NFKC" }], + }, + { + code: `{"${escapedNfkdO}":"NFKD"}`, + options: [{ form: "NFKD" }], + }, ], invalid: [ { code: `{"${o.normalize("NFD")}":"NFD"}`, + output: `{"${o.normalize("NFC")}":"NFD"}`, errors: [ { messageId: "unnormalizedKey", @@ -60,6 +83,7 @@ ruleTester.run("no-unnormalized-keys", rule, { }, { code: `{"${o.normalize("NFD")}":"NFD"}`, + output: `{"${o.normalize("NFC")}":"NFD"}`, language: "json/jsonc", errors: [ { @@ -72,8 +96,39 @@ ruleTester.run("no-unnormalized-keys", rule, { }, ], }, + { + code: `{"${o.normalize("NFD")}":"NFD"}`, + output: `{"${o.normalize("NFC")}":"NFD"}`, + language: "json/json5", + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFD") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, + { + code: `{'${o.normalize("NFD")}':'NFD'}`, + output: `{'${o.normalize("NFC")}':'NFD'}`, + language: "json/json5", + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFD") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 7, + }, + ], + }, { code: `{${o.normalize("NFD")}:"NFD"}`, + output: `{${o.normalize("NFC")}:"NFD"}`, language: "json/json5", errors: [ { @@ -88,6 +143,23 @@ ruleTester.run("no-unnormalized-keys", rule, { }, { code: `{"${o.normalize("NFKC")}":"NFKC"}`, + output: `{"${o.normalize("NFKD")}":"NFKC"}`, + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFKC") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: `{"${o.normalize("NFKC")}":"NFKC"}`, + output: `{"${o.normalize("NFKD")}":"NFKC"}`, + language: "json/jsonc", options: [{ form: "NFKD" }], errors: [ { @@ -100,5 +172,83 @@ ruleTester.run("no-unnormalized-keys", rule, { }, ], }, + { + code: `{"${o.normalize("NFKC")}":"NFKC"}`, + output: `{"${o.normalize("NFKD")}":"NFKC"}`, + language: "json/json5", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFKC") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: `{'${o.normalize("NFKC")}':"NFKC"}`, + output: `{'${o.normalize("NFKD")}':"NFKC"}`, + language: "json/json5", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFKC") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: `{${o.normalize("NFKC")}:"NFKC"}`, + output: `{${o.normalize("NFKD")}:"NFKC"}`, + language: "json/json5", + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFKC") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 3, + }, + ], + }, + // escaped form + { + code: `{"${escapedNfdO}":"NFD"}`, + output: `{"${o.normalize("NFC")}":"NFD"}`, + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFD") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 22, + }, + ], + }, + { + code: `{"${escapedNfkcO}":"NFKC"}`, + output: `{"${o.normalize("NFKD")}":"NFKC"}`, + options: [{ form: "NFKD" }], + errors: [ + { + messageId: "unnormalizedKey", + data: { key: o.normalize("NFKC") }, + line: 1, + column: 2, + endLine: 1, + endColumn: 10, + }, + ], + }, ], });