Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/rules/no-unnormalized-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const rule = {
meta: {
type: "problem",

fixable: "code",

docs: {
recommended: true,
description: "Disallow JSON keys that are not normalized",
Expand Down Expand Up @@ -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,
);
},
Comment on lines +73 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would produce incorrect fix if there are escape sequences, e.g., \n:

/* eslint json/no-unnormalized-keys: [2, { form: "NFD" }] */

{
    "\u1E9B\u0323\n": 42
}

Fixed to invalid JSON:

/* eslint json/no-unnormalized-keys: [2, { form: "NFD" }] */

{
    "ẛ̣
": 42
}

Also, I'm not sure if autofixing \u1E9B\u0323 to ẛ̣ instead of a sequence with \uXXXX would be desirable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion also makes sense to me, but if that's the case, I have two questions:

  1. Should data.key be updated accordingly?

    It seems data.key is displayed the same way:
    image

  2. Is there a JavaScript (or other) rule I can refer to?

    I don't have much experience with JSON (and JavaScript) rules, so I think I'm missing some edge cases while implementing the rule. Do you have any recommendations I could refer to when implementing the rule? If so, that would be very helpful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should data.key be updated accordingly?

I think it would be better to show the raw key representation (i.e., as it appears in the linted source code) in the error message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2. Is there a JavaScript (or other) rule I can refer to?

I think the most similar thing we have in JS rules is a utility that parses string literals:

https://github.com/eslint/eslint/blob/main/lib/rules/utils/char-source.js

So we could try making something similar for JSON and use it to check how individual characters were represented in the original key and then try preserving the same form (a character directly inserted into the source code or an escape sequence) in the fixed key. A problem that might be difficult to solve in this particular rule is how to map characters from the fixed key to characters in the original key since the normalizations produce strings with different lengths.

Another option is to limit the autofix to keys that don't have escape sequences only.

});
}
},
Expand Down
150 changes: 150 additions & 0 deletions tests/rules/no-unnormalized-keys.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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",
Expand All @@ -60,6 +83,7 @@ ruleTester.run("no-unnormalized-keys", rule, {
},
{
code: `{"${o.normalize("NFD")}":"NFD"}`,
output: `{"${o.normalize("NFC")}":"NFD"}`,
language: "json/jsonc",
errors: [
{
Expand All @@ -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: [
{
Expand All @@ -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: [
{
Expand All @@ -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,
},
],
},
],
});