diff --git a/README.md b/README.md index 4875eec..1abc34d 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ export default [ - `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally) - `no-unsafe-values` - warns on values that are unsafe for interchange, such as numbers outside safe range or lone surrogates. - `no-unnormalized-keys` - warns on keys containing [unnormalized characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize#description). You can optionally specify the normalization form via `{ form: "form_name" }`, where `form_name` can be any of `"NFC"`, `"NFD"`, `"NFKC"`, or `"NFKD"`. +- `top-level-interop` - warns when the top-level item in the document is neither an array nor an object. This can be enabled to ensure maximal interoperability with the oldest JSON parsers. ## Configuration Comments diff --git a/src/index.js b/src/index.js index 5113fe1..3006f9b 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import noDuplicateKeys from "./rules/no-duplicate-keys.js"; import noEmptyKeys from "./rules/no-empty-keys.js"; import noUnsafeValues from "./rules/no-unsafe-values.js"; import noUnnormalizedKeys from "./rules/no-unnormalized-keys.js"; +import topLevelInterop from "./rules/top-level-interop.js"; //----------------------------------------------------------------------------- // Plugin @@ -33,6 +34,7 @@ const plugin = { "no-empty-keys": noEmptyKeys, "no-unsafe-values": noUnsafeValues, "no-unnormalized-keys": noUnnormalizedKeys, + "top-level-interop": topLevelInterop, }, configs: { recommended: { diff --git a/src/rules/top-level-interop.js b/src/rules/top-level-interop.js new file mode 100644 index 0000000..ea4d7fa --- /dev/null +++ b/src/rules/top-level-interop.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Rule to ensure top-level items are either an array or ojbect. + * @author Joe Hildebrand + */ + +export default { + meta: { + type: /** @type {const} */ ("problem"), + + docs: { + description: + "Require the JSON top-level value to be an array or object", + }, + + messages: { + topLevel: + "Top level item should be array or object, got '{{type}}'.", + }, + }, + + create(context) { + return { + Document(node) { + const { type } = node.body; + if (type !== "Object" && type !== "Array") { + context.report({ + loc: node.loc, + messageId: "topLevel", + data: { type }, + }); + } + }, + }; + }, +}; diff --git a/tests/rules/top-level-interop.test.js b/tests/rules/top-level-interop.test.js new file mode 100644 index 0000000..20eca5b --- /dev/null +++ b/tests/rules/top-level-interop.test.js @@ -0,0 +1,108 @@ +/** + * @fileoverview Tests for top-level-interop rule. + * @author Joe Hildebrand + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import rule from "../../src/rules/top-level-interop.js"; +import json from "../../src/index.js"; +import { RuleTester } from "eslint"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + plugins: { + json, + }, + language: "json/json", +}); + +ruleTester.run("top-level-interop", rule, { + valid: [ + "[]", + { + code: "[1]", + language: "json/json5", + }, + { + code: "[1, 2]", + language: "json/json5", + }, + "{}", + { + code: '{"foo": 1}', + language: "json/json5", + }, + { + code: '{"foo": 1, "foo": 2}', + language: "json/json5", + }, + ], + invalid: [ + { + code: "1", + errors: [ + { + messageId: "topLevel", + data: { + type: "Number", + }, + line: 1, + column: 1, + endLine: 1, + endColumn: 2, + }, + ], + }, + { + code: "true", + errors: [ + { + messageId: "topLevel", + data: { + type: "Boolean", + }, + line: 1, + column: 1, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: "null", + errors: [ + { + messageId: "topLevel", + data: { + type: "Null", + }, + line: 1, + column: 1, + endLine: 1, + endColumn: 5, + }, + ], + }, + { + code: '"foo"', + errors: [ + { + messageId: "topLevel", + data: { + type: "String", + }, + line: 1, + column: 1, + endLine: 1, + endColumn: 6, + }, + ], + }, + ], +});