Skip to content

Commit fc3beec

Browse files
authored
feat: implement autofix for no-unnormalized-keys (#151)
* feat: implement autofix for `no-unnormalized-keys` * wip * wip: pin `rollup` * wip: add more test cases * wip * wip * wip: add more test * fix: display raw key in the message * wip: add more test cases * wip: only perform auto-fix when the raw key does not contain escape sequences
1 parent 87ea56f commit fc3beec

File tree

2 files changed

+243
-4
lines changed

2 files changed

+243
-4
lines changed

src/rules/no-unnormalized-keys.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Imports
88
//-----------------------------------------------------------------------------
99

10-
import { getKey } from "../util.js";
10+
import { getKey, getRawKey } from "../util.js";
1111

1212
//-----------------------------------------------------------------------------
1313
// Type Definitions
@@ -29,6 +29,8 @@ const rule = {
2929
meta: {
3030
type: "problem",
3131

32+
fixable: "code",
33+
3234
docs: {
3335
recommended: true,
3436
description: "Disallow JSON keys that are not normalized",
@@ -64,13 +66,30 @@ const rule = {
6466
return {
6567
Member(node) {
6668
const key = getKey(node);
69+
const rawKey = getRawKey(node, context.sourceCode);
70+
const normalizedKey = key.normalize(form);
71+
72+
if (normalizedKey !== key) {
73+
const { name } = node;
6774

68-
if (key.normalize(form) !== key) {
6975
context.report({
70-
loc: node.name.loc,
76+
loc: name.loc,
7177
messageId: "unnormalizedKey",
7278
data: {
73-
key,
79+
key: rawKey,
80+
},
81+
fix(fixer) {
82+
if (key !== rawKey) {
83+
// Do not perform auto-fix when the raw key contains escape sequences.
84+
return null;
85+
}
86+
87+
return fixer.replaceTextRange(
88+
name.type === "String"
89+
? [name.range[0] + 1, name.range[1] - 1]
90+
: name.range,
91+
normalizedKey,
92+
);
7493
},
7594
});
7695
}

tests/rules/no-unnormalized-keys.test.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const ruleTester = new RuleTester({
2323
});
2424

2525
const o = "\u1E9B\u0323";
26+
const escapedNfcO = "\\u1E9B\\u0323";
27+
const escapedNfdO = "\\u017F\\u0323\\u0307";
28+
const escapedNfkcO = "\\u1E69";
29+
const escapedNfkdO = "\\u0073\\u0323\\u0307";
2630

2731
ruleTester.run("no-unnormalized-keys", rule, {
2832
valid: [
@@ -43,10 +47,69 @@ ruleTester.run("no-unnormalized-keys", rule, {
4347
code: `{"${o.normalize("NFKD")}":"NFKD"}`,
4448
options: [{ form: "NFKD" }],
4549
},
50+
// escaped form
51+
`{"${escapedNfcO}":"NFC"}`,
52+
{
53+
code: `{"${escapedNfcO}":"NFC"}`,
54+
options: [{ form: "NFC" }],
55+
},
56+
{
57+
code: `{"${escapedNfcO}":"NFC"}`,
58+
language: "json/jsonc",
59+
options: [{ form: "NFC" }],
60+
},
61+
{
62+
code: `{"${escapedNfcO}":"NFC"}`,
63+
language: "json/json5",
64+
options: [{ form: "NFC" }],
65+
},
66+
{
67+
code: `{"${escapedNfdO}":"NFD"}`,
68+
options: [{ form: "NFD" }],
69+
},
70+
{
71+
code: `{"${escapedNfdO}":"NFD"}`,
72+
language: "json/jsonc",
73+
options: [{ form: "NFD" }],
74+
},
75+
{
76+
code: `{"${escapedNfdO}":"NFD"}`,
77+
language: "json/json5",
78+
options: [{ form: "NFD" }],
79+
},
80+
{
81+
code: `{"${escapedNfkcO}":"NFKC"}`,
82+
options: [{ form: "NFKC" }],
83+
},
84+
{
85+
code: `{"${escapedNfkcO}":"NFKC"}`,
86+
language: "json/jsonc",
87+
options: [{ form: "NFKC" }],
88+
},
89+
{
90+
code: `{"${escapedNfkcO}":"NFKC"}`,
91+
language: "json/json5",
92+
options: [{ form: "NFKC" }],
93+
},
94+
{
95+
code: `{"${escapedNfkdO}":"NFKD"}`,
96+
options: [{ form: "NFKD" }],
97+
},
98+
{
99+
code: `{"${escapedNfkdO}":"NFKD"}`,
100+
language: "json/jsonc",
101+
options: [{ form: "NFKD" }],
102+
},
103+
{
104+
code: `{"${escapedNfkdO}":"NFKD"}`,
105+
language: "json/json5",
106+
options: [{ form: "NFKD" }],
107+
},
46108
],
47109
invalid: [
48110
{
49111
code: `{"${o.normalize("NFD")}":"NFD"}`,
112+
output: `{"${o.normalize("NFC")}":"NFD"}`,
50113
errors: [
51114
{
52115
messageId: "unnormalizedKey",
@@ -60,6 +123,7 @@ ruleTester.run("no-unnormalized-keys", rule, {
60123
},
61124
{
62125
code: `{"${o.normalize("NFD")}":"NFD"}`,
126+
output: `{"${o.normalize("NFC")}":"NFD"}`,
63127
language: "json/jsonc",
64128
errors: [
65129
{
@@ -72,8 +136,39 @@ ruleTester.run("no-unnormalized-keys", rule, {
72136
},
73137
],
74138
},
139+
{
140+
code: `{"${o.normalize("NFD")}":"NFD"}`,
141+
output: `{"${o.normalize("NFC")}":"NFD"}`,
142+
language: "json/json5",
143+
errors: [
144+
{
145+
messageId: "unnormalizedKey",
146+
data: { key: o.normalize("NFD") },
147+
line: 1,
148+
column: 2,
149+
endLine: 1,
150+
endColumn: 7,
151+
},
152+
],
153+
},
154+
{
155+
code: `{'${o.normalize("NFD")}':'NFD'}`,
156+
output: `{'${o.normalize("NFC")}':'NFD'}`,
157+
language: "json/json5",
158+
errors: [
159+
{
160+
messageId: "unnormalizedKey",
161+
data: { key: o.normalize("NFD") },
162+
line: 1,
163+
column: 2,
164+
endLine: 1,
165+
endColumn: 7,
166+
},
167+
],
168+
},
75169
{
76170
code: `{${o.normalize("NFD")}:"NFD"}`,
171+
output: `{${o.normalize("NFC")}:"NFD"}`,
77172
language: "json/json5",
78173
errors: [
79174
{
@@ -88,6 +183,23 @@ ruleTester.run("no-unnormalized-keys", rule, {
88183
},
89184
{
90185
code: `{"${o.normalize("NFKC")}":"NFKC"}`,
186+
output: `{"${o.normalize("NFKD")}":"NFKC"}`,
187+
options: [{ form: "NFKD" }],
188+
errors: [
189+
{
190+
messageId: "unnormalizedKey",
191+
data: { key: o.normalize("NFKC") },
192+
line: 1,
193+
column: 2,
194+
endLine: 1,
195+
endColumn: 5,
196+
},
197+
],
198+
},
199+
{
200+
code: `{"${o.normalize("NFKC")}":"NFKC"}`,
201+
output: `{"${o.normalize("NFKD")}":"NFKC"}`,
202+
language: "json/jsonc",
91203
options: [{ form: "NFKD" }],
92204
errors: [
93205
{
@@ -100,5 +212,113 @@ ruleTester.run("no-unnormalized-keys", rule, {
100212
},
101213
],
102214
},
215+
{
216+
code: `{"${o.normalize("NFKC")}":"NFKC"}`,
217+
output: `{"${o.normalize("NFKD")}":"NFKC"}`,
218+
language: "json/json5",
219+
options: [{ form: "NFKD" }],
220+
errors: [
221+
{
222+
messageId: "unnormalizedKey",
223+
data: { key: o.normalize("NFKC") },
224+
line: 1,
225+
column: 2,
226+
endLine: 1,
227+
endColumn: 5,
228+
},
229+
],
230+
},
231+
{
232+
code: `{'${o.normalize("NFKC")}':"NFKC"}`,
233+
output: `{'${o.normalize("NFKD")}':"NFKC"}`,
234+
language: "json/json5",
235+
options: [{ form: "NFKD" }],
236+
errors: [
237+
{
238+
messageId: "unnormalizedKey",
239+
data: { key: o.normalize("NFKC") },
240+
line: 1,
241+
column: 2,
242+
endLine: 1,
243+
endColumn: 5,
244+
},
245+
],
246+
},
247+
{
248+
code: `{${o.normalize("NFKC")}:"NFKC"}`,
249+
output: `{${o.normalize("NFKD")}:"NFKC"}`,
250+
language: "json/json5",
251+
options: [{ form: "NFKD" }],
252+
errors: [
253+
{
254+
messageId: "unnormalizedKey",
255+
data: { key: o.normalize("NFKC") },
256+
line: 1,
257+
column: 2,
258+
endLine: 1,
259+
endColumn: 3,
260+
},
261+
],
262+
},
263+
// escaped form
264+
{
265+
code: `{"${escapedNfcO}":"NFC"}`,
266+
// No auto-fix due to the presence of escape sequences in the raw key.
267+
options: [{ form: "NFD" }],
268+
errors: [
269+
{
270+
messageId: "unnormalizedKey",
271+
data: { key: escapedNfcO },
272+
line: 1,
273+
column: 2,
274+
endLine: 1,
275+
endColumn: 16,
276+
},
277+
],
278+
},
279+
{
280+
code: `{"${escapedNfcO}\\n":42}`,
281+
// No auto-fix due to the presence of escape sequences in the raw key.
282+
options: [{ form: "NFD" }],
283+
errors: [
284+
{
285+
messageId: "unnormalizedKey",
286+
data: { key: `${escapedNfcO}\\n` },
287+
line: 1,
288+
column: 2,
289+
endLine: 1,
290+
endColumn: 18,
291+
},
292+
],
293+
},
294+
{
295+
code: `{"${escapedNfdO}":"NFD"}`,
296+
// No auto-fix due to the presence of escape sequences in the raw key.
297+
errors: [
298+
{
299+
messageId: "unnormalizedKey",
300+
data: { key: escapedNfdO },
301+
line: 1,
302+
column: 2,
303+
endLine: 1,
304+
endColumn: 22,
305+
},
306+
],
307+
},
308+
{
309+
code: `{"${escapedNfkcO}":"NFKC"}`,
310+
// No auto-fix due to the presence of escape sequences in the raw key.
311+
options: [{ form: "NFKD" }],
312+
errors: [
313+
{
314+
messageId: "unnormalizedKey",
315+
data: { key: escapedNfkcO },
316+
line: 1,
317+
column: 2,
318+
endLine: 1,
319+
endColumn: 10,
320+
},
321+
],
322+
},
103323
],
104324
});

0 commit comments

Comments
 (0)