Skip to content

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

Open
electrohyun wants to merge 5 commits into
eslint:mainfrom
electrohyun:fix/no-unnormalized-keys-escaping
Open

electrohyun wants to merge 5 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.

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

  • Refactor

    • Refined internal handling of escaped characters during automatic fixes for unnormalized string keys.
    • Existing behavior and generated fixes remain unchanged.
  • Tests

    • Updated test representations for full-width quotation marks, apostrophes, and backslashes using Unicode escape sequences.
    • Preserved coverage 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: ee79dbec-bb39-414c-a60f-b6262b8052ec

📥 Commits

Reviewing files that changed from the base of the PR and between 90d3342 and f621ec2.

📒 Files selected for processing (2)
  • src/rules/no-unnormalized-keys.js
  • tests/rules/no-unnormalized-keys.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/rules/no-unnormalized-keys.test.js

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain 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 and escapes backslashes and matching quote characters before replacement. Tests cover JSON, JSONC, and JSON5 with NFKC and NFKD normalization.

Priority: ⚪ Pending latest changes

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

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: lumirlumir

Merge Risk: ⚪ Minimal · up to 42ff3

The autofix preserves quote style and escapes normalized quotes and backslashes across the supported JSON formats, with matching regression coverage.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing escaping in the no-unnormalized-keys autofix so normalized keys remain valid.
Linked Issues check ✅ Passed Issue #282 requires the no-unnormalized-keys autofix to keep normalized keys valid when NFKC/NFKD introduces quotes or backslashes. The rule now escapes backslashes and the matching delimiter in quo…
Out of Scope Changes check ✅ Passed The changes are limited to the no-unnormalized-keys rule and its regression tests. These changes directly support issue #282 and add no unrelated behavior or files.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 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 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,
							);

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: Second Review Needed

Development

Successfully merging this pull request may close these issues.

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

4 participants