Skip to content

fix: escape normalized keys in no-unnormalized-keys autofix - #283

Merged
lumirlumir merged 7 commits into
eslint:mainfrom
electrohyun:fix/no-unnormalized-keys-escaping
Sep 24, 2026
Merged

lumirlumir merged 7 commits into
eslint:mainfrom
electrohyun:fix/no-unnormalized-keys-escaping

Conversation

@electrohyun

@electrohyun electrohyun commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Prerequisites checklist

AI acknowledgment

  • I did not use AI to generate this PR.
  • (If the above is not checked) I have reviewed the AI-generated content before submitting.

I used AI to check the English grammar in review responses I wrote myself. The technical decisions in those responses were my own.

What is the purpose of this pull request?

Fix no-unnormalized-keys autofix producing invalid JSON when normalization introduces quotes or \.

What changes did you make? (Give an overview)

Escape \ and matching quotes in normalized keys.

Add regression tests for NFKC and NFKD normalization across JSON, JSONC, and JSON5 (single quote cases are included only in JSON5).

Related Issues

fixes #282

Is there anything you'd like reviewers to focus on?

At first, I wrote code with a let variable, but it ended up using that variable’s name instead of normalizedKey, and I think this is less consistent with the rule name no-unnormalized-keys.

The current approach does the same by using a different name, escapedKey, but only for string keys and normalizedKey is still used for identifier keys, so I went with this structure.

If there’s a better way to structure the fixer, please let me know.


Disclosure: I'm a participant of open source contribution program OSSCA

Summary by CodeRabbit

  • Bug Fixes

    • Improved automatic fixes for unnormalized string keys by preserving their original quote style and correctly escaping quotation marks and backslashes.
    • Ensured normalized keys are inserted safely across supported key formats.
  • Tests

    • Expanded coverage for full-width quotation marks, apostrophes, and backslashes using Unicode escape sequences.
    • Preserved validation across supported normalization forms and JSON-derived formats.

@eslint-github-bot eslint-github-bot Bot added the bug Something isn't working label Sep 8, 2026
@eslintbot eslintbot added this to Triage Sep 8, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in Triage Sep 8, 2026
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: de878494-1ea5-48fc-9754-d159d88b2e26

📥 Commits

Reviewing files that changed from the base of the PR and between 42ff357 and 71c2856.

📒 Files selected for processing (1)
  • src/rules/no-unnormalized-keys.js

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The no-unnormalized-keys autofix now escapes backslashes and quote characters in normalized string keys. Tests represent full-width characters with Unicode escapes while preserving existing assertions and autofix outputs.

Changes

Normalized key autofix

Layer / File(s) Summary
Escape normalized string keys and validate fixes
src/rules/no-unnormalized-keys.js, tests/rules/no-unnormalized-keys.test.js
The rule reads the source quote, escapes backslashes and matching quote characters, and replaces only the key contents. Tests cover JSON, JSONC, and JSON5 with NFKC and NFKD normalization.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: lumirlumir

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #282 requires the no-unnormalized-keys autofix to produce valid normalized keys when NFKC or NFKD introduces quote or backslash characters. The change escapes backslashes and the original quot…
Out of Scope Changes check ✅ Passed The changes are limited to src/rules/no-unnormalized-keys.js and its regression tests. The implementation and tests directly support issue #282. No unrelated change is identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: escaping normalized keys in the no-unnormalized-keys autofix.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

},
{
code: `{"a"b\c'"\": 1}`,
output: `{"a\\"b\\\\c'\\"\\\\": 1}`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should we use String.raw for readability here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree that using String.raw for every output property would be more readable.

@DMartens DMartens left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thank you for the PR.
The logic looks good to me, just one suggested refactoring and additional test cases.

Comment thread src/rules/no-unnormalized-keys.js Outdated
: name.range,
normalizedKey,
);
if (name.type === "String") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We could simplify this as only the "key" is changed:

const fixedKey = name.type === "String" ? escapeKey(...) : normalizedKey;
return fixer.replaceText(name, fixedKey);

and extract the quote + replaceAll logic into a function (which now adds the same quotes around the key as the whole key is replaced).
As you mentioned it is hard to come up with a better name as normalizedKey is in scope.

],
},
{
code: `{"a"b\c'"\": 1}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you please add separate test cases for one options.form for the quotes and escaping logic (e.g. { "a'b": 1 } showing that the not used quote is not escaped).
You can keep the "combined" test cases.

@DMartens DMartens moved this from Needs Triage to Implementing in Triage Sep 8, 2026
@electrohyun

Copy link
Copy Markdown
Contributor Author

@DMartens Applied. I think fixedKey looks good. Thank you for the review!

],
},
{
code: `{"a"b": 1}`,

@lumirlumir lumirlumir Sep 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
code: `{"a"b": 1}`,
code: `{"a\uff02b": 1}`,

Disclosure: I'm a participant of open source contribution program OSSCA: confirmed.

If using U+FF02 is the intention of this test, could we use the Unicode escape sequence instead? In some code editors or IDEs, it may look similar to U+0022, which could confuse people when reading the test cases.

It would also be helpful to update the other test cases affected by this.

Comment thread src/rules/no-unnormalized-keys.js Outdated
);
? escapeKey(
normalizedKey,
context.sourceCode.getText(name),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
context.sourceCode.getText(name),
sourceCode.getText(name),
+ const { sourceCode } = context;
const [{ form }] = context.options;

Small suggestion: I think we could destructure sourceCode at the top to avoid the minor overhead of accessing it each time, since there’s one more occurrence of context.sourceCode.

Comment thread src/rules/no-unnormalized-keys.js Outdated
);
? escapeKey(
normalizedKey,
context.sourceCode.getText(name),

@lumirlumir lumirlumir Sep 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure we need to call the getText method here. We can simply access the quote information as follows:

							const fixedKey =
								name.type === "String"
									? escapeKey(
											normalizedKey,
											context.sourceCode.text[
												name.range[0]
											],
										)
									: normalizedKey;

The helper function could then be simplified as follows:

/**
 * 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}`;
}

@electrohyun

Copy link
Copy Markdown
Contributor Author

@lumirlumir Applied. Thank you for suggesting this structure!

DMartens
DMartens previously approved these changes Sep 15, 2026

@DMartens DMartens left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes LGTM, thanks. Leaving open for lumir.

@DMartens DMartens moved this from Implementing to Second Review Needed in Triage Sep 15, 2026
Comment thread src/rules/no-unnormalized-keys.js Outdated
Comment on lines +107 to +115
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The behavior would be the same, but rather than reconstructing the key by concatenating strings based on the quote information, I think we can simply escape the normalized key here.

To me, the original type === "String" ? [range[0] + 1, range[1] - 1] : range more clearly represents the original intent and makes it clear exactly where this fix applies.

/**
 * 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 and quoted key.
 */
function escapeKey(normalizedKey, quote) {
	return normalizedKey
		.replaceAll("\\", "\\\\")
		.replaceAll(quote, `\\${quote}`);
}
const { loc, range, type } = node.name;
							return fixer.replaceTextRange(
								type === "String"
									? [range[0] + 1, range[1] - 1]
									: range,
								type === "String"
									? escapeKey(
											normalizedKey,
											sourceCode.text[range[0]],
										)
									: normalizedKey,
							);

@electrohyun

Copy link
Copy Markdown
Contributor Author

Destructuring node.name and simplifying the helper function look good too.

I’ll keep the original approach while retaining the helper extraction suggested in the earlier review. Thanks for another review!

@DMartens DMartens left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes LGTM, thanks. Leaving open for lumir.

@lumirlumir lumirlumir left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks!

@lumirlumir
lumirlumir merged commit 35790d4 into eslint:main Sep 24, 2026
41 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accepted bug Something isn't working

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

Bug: no-unnormalized-keys autofix normalizes key without escaping " or \

4 participants