diff --git a/src/rules/no-duplicate-keys.js b/src/rules/no-duplicate-keys.js index fef2ffa..73ede6d 100644 --- a/src/rules/no-duplicate-keys.js +++ b/src/rules/no-duplicate-keys.js @@ -3,6 +3,12 @@ * @author Nicholas C. Zakas */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import { getKey, getRawKey } from "../util.js"; + //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- @@ -49,23 +55,22 @@ const rule = { }, Member(node) { - const key = - node.name.type === "String" - ? node.name.value - : node.name.name; + const key = getKey(node); + const rawKey = getRawKey(node, context.sourceCode); if (keys.has(key)) { context.report({ loc: node.name.loc, messageId: "duplicateKey", data: { - key, + key: rawKey, }, }); } else { keys.set(key, node); } }, + "Object:exit"() { keys = objectKeys.pop(); }, diff --git a/src/rules/no-empty-keys.js b/src/rules/no-empty-keys.js index 78fcd66..067d406 100644 --- a/src/rules/no-empty-keys.js +++ b/src/rules/no-empty-keys.js @@ -3,6 +3,12 @@ * @author Nicholas C. Zakas */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import { getKey } from "../util.js"; + //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- @@ -37,10 +43,7 @@ const rule = { create(context) { return { Member(node) { - const key = - node.name.type === "String" - ? node.name.value - : node.name.name; + const key = getKey(node); if (key.trim() === "") { context.report({ diff --git a/src/rules/no-unnormalized-keys.js b/src/rules/no-unnormalized-keys.js index 8794cff..2bb7b0c 100644 --- a/src/rules/no-unnormalized-keys.js +++ b/src/rules/no-unnormalized-keys.js @@ -3,6 +3,12 @@ * @author Bradley Meck Farias */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import { getKey } from "../util.js"; + //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- @@ -58,10 +64,7 @@ const rule = { return { Member(node) { - const key = - node.name.type === "String" - ? node.name.value - : node.name.name; + const key = getKey(node); if (key.normalize(form) !== key) { context.report({ diff --git a/src/rules/sort-keys.js b/src/rules/sort-keys.js index ce4ceec..20dc797 100644 --- a/src/rules/sort-keys.js +++ b/src/rules/sort-keys.js @@ -9,6 +9,7 @@ //----------------------------------------------------------------------------- import naturalCompare from "natural-compare"; +import { getKey, getRawKey } from "../util.js"; //----------------------------------------------------------------------------- // Type Definitions @@ -76,17 +77,6 @@ const comparators = { }, }; -/** - * Gets the MemberNode's string key value. - * @param {MemberNode} member - * @return {string} - */ -function getKey(member) { - return member.name.type === "Identifier" - ? member.name.name - : member.name.value; -} - //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- @@ -144,6 +134,7 @@ const rule = { }, create(context) { + const { sourceCode } = context; const [ directionShort, { allowLineSeparatedGroups, caseSensitive, natural, minKeys }, @@ -156,7 +147,7 @@ const rule = { // Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment const commentLineNums = new Set(); - for (const comment of context.sourceCode.comments) { + for (const comment of sourceCode.comments) { for ( let lineNum = comment.loc.start.line; lineNum <= comment.loc.end.line; @@ -187,9 +178,7 @@ const rule = { ) { if ( !commentLineNums.has(lineNum) && - !hasNonWhitespace.test( - context.sourceCode.lines[lineNum - 1], - ) + !hasNonWhitespace.test(sourceCode.lines[lineNum - 1]) ) { return true; } @@ -200,8 +189,12 @@ const rule = { return { Object(node) { + /** @type {MemberNode} */ let prevMember; + /** @type {string} */ let prevName; + /** @type {string} */ + let prevRawName; if (node.members.length < minKeys) { return; @@ -209,6 +202,7 @@ const rule = { for (const member of node.members) { const thisName = getKey(member); + const thisRawName = getRawKey(member, sourceCode); if ( prevMember && @@ -220,8 +214,8 @@ const rule = { loc: member.name.loc, messageId: "sortKeys", data: { - thisName, - prevName, + thisName: thisRawName, + prevName: prevRawName, direction, sensitivity, sortName, @@ -231,6 +225,7 @@ const rule = { prevMember = member; prevName = thisName; + prevRawName = thisRawName; } }, }; diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..f87a26a --- /dev/null +++ b/src/util.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Utility Library + * @author 루밀LuMir(lumirlumir) + */ + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** + * @import { MemberNode } from "@humanwhocodes/momoa"; + * @import { JSONSourceCode } from "./languages/json-source-code.js"; + */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Gets the `MemberNode`'s key value. + * @param {MemberNode} node The node to get the key from. + * @returns {string} The key value. + */ +export function getKey(node) { + return node.name.type === "String" ? node.name.value : node.name.name; +} + +/** + * Gets the `MemberNode`'s raw key value. + * @param {MemberNode} node The node to get the raw key from. + * @param {JSONSourceCode} sourceCode The JSON source code object. + * @returns {string} The raw key value. + */ +export function getRawKey(node, sourceCode) { + return node.name.type === "String" + ? sourceCode.getText(node.name, -1, -1) + : sourceCode.getText(node.name); +} diff --git a/tests/rules/no-duplicate-keys.test.js b/tests/rules/no-duplicate-keys.test.js index e84bd87..65f0076 100644 --- a/tests/rules/no-duplicate-keys.test.js +++ b/tests/rules/no-duplicate-keys.test.js @@ -151,12 +151,67 @@ ruleTester.run("no-duplicate-keys", rule, { }, ], }, + { + code: '{"foot": 1, "fo\\u006ft": 2}', + errors: [ + { + messageId: "duplicateKey", + data: { key: "fo\\u006ft" }, + line: 1, + column: 13, + endLine: 1, + endColumn: 24, + }, + ], + }, + { + code: '{"foot": 1, "fo\\u006ft": 2}', + language: "json/jsonc", + errors: [ + { + messageId: "duplicateKey", + data: { key: "fo\\u006ft" }, + line: 1, + column: 13, + endLine: 1, + endColumn: 24, + }, + ], + }, + { + code: '{"foot": 1, "fo\\u006ft": 2}', + language: "json/json5", + errors: [ + { + messageId: "duplicateKey", + data: { key: "fo\\u006ft" }, + line: 1, + column: 13, + endLine: 1, + endColumn: 24, + }, + ], + }, + { + code: "{foot: 1, fo\\u006ft: 2}", + language: "json/json5", + errors: [ + { + messageId: "duplicateKey", + data: { key: "fo\\u006ft" }, + line: 1, + column: 11, + endLine: 1, + endColumn: 20, + }, + ], + }, { code: '{"f\\u006fot": 1, "fo\\u006ft": 2}', errors: [ { messageId: "duplicateKey", - data: { key: "foot" }, + data: { key: "fo\\u006ft" }, line: 1, column: 18, endLine: 1, @@ -170,7 +225,7 @@ ruleTester.run("no-duplicate-keys", rule, { errors: [ { messageId: "duplicateKey", - data: { key: "foot" }, + data: { key: "fo\\u006ft" }, line: 1, column: 18, endLine: 1, @@ -184,7 +239,7 @@ ruleTester.run("no-duplicate-keys", rule, { errors: [ { messageId: "duplicateKey", - data: { key: "foot" }, + data: { key: "fo\\u006ft" }, line: 1, column: 18, endLine: 1, @@ -198,7 +253,7 @@ ruleTester.run("no-duplicate-keys", rule, { errors: [ { messageId: "duplicateKey", - data: { key: "foot" }, + data: { key: "fo\\u006ft" }, line: 1, column: 16, endLine: 1, diff --git a/tests/rules/sort-keys.test.js b/tests/rules/sort-keys.test.js index c15bb15..f50f510 100644 --- a/tests/rules/sort-keys.test.js +++ b/tests/rules/sort-keys.test.js @@ -2029,5 +2029,45 @@ ruleTester.run("sort-keys", rule, { }, ], }, + + // Escape sequences in keys + { + code: '{"\\u0061":1, "\\u0063":2, "\\u0062":3}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "\\u0062", + prevName: "\\u0063", + }, + line: 1, + column: 26, + endLine: 1, + endColumn: 34, + }, + ], + }, + { + code: '{"\\u0061\\n":1, "\\u0063\\n":2, "\\u0062\\n":3}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "\\u0062\\n", + prevName: "\\u0063\\n", + }, + line: 1, + column: 30, + endLine: 1, + endColumn: 40, + }, + ], + }, ], }); diff --git a/tests/util.test.js b/tests/util.test.js new file mode 100644 index 0000000..2e68f78 --- /dev/null +++ b/tests/util.test.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Tests for util.js + * @author 루밀LuMir(lumirlumir) + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import assert from "node:assert"; +import { JSONLanguage } from "../src/languages/json-language.js"; +import { JSONSourceCode } from "../src/languages/json-source-code.js"; +import { getKey, getRawKey } from "../src/util.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("util", () => { + describe("getKey()", () => { + it("should return the correct key for `String` nodes", () => { + const node = { + name: { type: "String", value: "value" }, + }; + + assert.strictEqual(getKey(node), "value"); + }); + + it("should return the correct key for `Identifier` nodes", () => { + const node = { + name: { type: "Identifier", name: "name" }, + }; + + assert.strictEqual(getKey(node), "name"); + }); + }); + + describe("getRawKey()", () => { + const file = { body: `{"foo": 1, 'bar': 2, baz: 3}` }; + const language = new JSONLanguage({ mode: "json5" }); + const parseResult = language.parse(file); + const sourceCode = new JSONSourceCode({ + text: file.body, + ast: parseResult.ast, + }); + + it("should return correct raw key for `String` nodes with double quotes", () => { + const fooNode = parseResult.ast.body.members[0]; + + assert.strictEqual(getRawKey(fooNode, sourceCode), "foo"); + }); + + it("should return correct raw key for `String` nodes with single quotes", () => { + const barNode = parseResult.ast.body.members[1]; + + assert.strictEqual(getRawKey(barNode, sourceCode), "bar"); + }); + + it("should return correct raw key for `Identifier` nodes", () => { + const bazNode = parseResult.ast.body.members[2]; + + assert.strictEqual(getRawKey(bazNode, sourceCode), "baz"); + }); + }); +});