diff --git a/eslint.config.js b/eslint.config.js index e797733..2f2b003 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,6 +9,7 @@ import eslintConfigESLint from "eslint-config-eslint"; import eslintPlugin from "eslint-plugin-eslint-plugin"; +import globals from "globals"; import json from "./src/index.js"; import { defineConfig, globalIgnores } from "eslint/config"; @@ -16,10 +17,6 @@ import { defineConfig, globalIgnores } from "eslint/config"; // Helpers //----------------------------------------------------------------------------- -const eslintPluginJSDoc = eslintConfigESLint.find( - config => config.plugins?.jsdoc, -).plugins.jsdoc; - const eslintPluginRulesRecommendedConfig = eslintPlugin.configs["flat/rules-recommended"]; const eslintPluginTestsRecommendedConfig = @@ -30,54 +27,34 @@ const eslintPluginTestsRecommendedConfig = //----------------------------------------------------------------------------- export default defineConfig([ - globalIgnores([ - "**/tests/fixtures/", - "**/dist/", - "coverage/", - "src/build/", - ]), - - ...eslintConfigESLint.map(config => ({ - files: ["**/*.js"], - ...config, - })), - { - plugins: { json }, - files: ["**/*.json", ".c8rc"], - language: "json/json", - extends: ["json/recommended"], - }, + globalIgnores(["coverage/", "dist/", "src/build/"], "json/global-ignores"), { + name: "json/js", files: ["**/*.js"], + extends: [eslintConfigESLint], rules: { - // disable rules we don't want to use from eslint-config-eslint "no-undefined": "off", - - // TODO: re-enable eslint-plugin-jsdoc rules - ...Object.fromEntries( - Object.keys(eslintPluginJSDoc.rules).map(name => [ - `jsdoc/${name}`, - "off", - ]), - ), }, }, { - files: ["**/tests/**"], + name: "json/tools", + files: ["tools/**/*.js"], + rules: { + "no-console": "off", + }, + }, + { + name: "json/tests", + files: ["tests/**/*.js"], + ignores: ["tests/rules/*.js"], languageOptions: { globals: { - describe: "readonly", - xdescribe: "readonly", - it: "readonly", - xit: "readonly", - beforeEach: "readonly", - afterEach: "readonly", - before: "readonly", - after: "readonly", + ...globals.mocha, }, }, }, { + name: "json/rules", files: ["src/rules/*.js"], extends: [eslintPluginRulesRecommendedConfig], rules: { @@ -99,6 +76,7 @@ export default defineConfig([ }, }, { + name: "json/rules-tests", files: ["tests/rules/*.test.js"], extends: [eslintPluginTestsRecommendedConfig], rules: { @@ -119,9 +97,10 @@ export default defineConfig([ }, }, { - files: ["tools/**/*.js"], - rules: { - "no-console": "off", - }, + name: "json/json", + plugins: { json }, + files: ["**/*.json", ".c8rc"], + language: "json/json", + extends: ["json/recommended"], }, ]); diff --git a/package.json b/package.json index c9fc9fc..be6db03 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "eslint": "^9.36.0", "eslint-config-eslint": "^13.0.0", "eslint-plugin-eslint-plugin": "^6.3.2", + "globals": "^16.5.0", "lint-staged": "^15.2.7", "mdast-util-from-markdown": "^2.0.2", "mocha": "^11.3.0", diff --git a/src/languages/json-language.js b/src/languages/json-language.js index 78ee1a8..68f2195 100644 --- a/src/languages/json-language.js +++ b/src/languages/json-language.js @@ -18,10 +18,8 @@ import { visitorKeys } from "@humanwhocodes/momoa"; /** * @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa"; * @import { Language, OkParseResult, ParseResult, File } from "@eslint/core"; - * * @typedef {OkParseResult} JSONOkParseResult * @typedef {ParseResult} JSONParseResult - * * @typedef {Object} JSONLanguageOptions * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode. */ diff --git a/src/rules/no-duplicate-keys.js b/src/rules/no-duplicate-keys.js index 73ede6d..05fa9ec 100644 --- a/src/rules/no-duplicate-keys.js +++ b/src/rules/no-duplicate-keys.js @@ -16,7 +16,6 @@ import { getKey, getRawKey } from "../util.js"; /** * @import { MemberNode } from "@humanwhocodes/momoa"; * @import { JSONRuleDefinition } from "../types.ts"; - * * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition */ diff --git a/src/rules/no-empty-keys.js b/src/rules/no-empty-keys.js index 067d406..7422488 100644 --- a/src/rules/no-empty-keys.js +++ b/src/rules/no-empty-keys.js @@ -15,7 +15,6 @@ import { getKey } from "../util.js"; /** * @import { JSONRuleDefinition } from "../types.ts"; - * * @typedef {"emptyKey"} NoEmptyKeysMessageIds * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition */ diff --git a/src/rules/no-unnormalized-keys.js b/src/rules/no-unnormalized-keys.js index 2bb7b0c..5baa669 100644 --- a/src/rules/no-unnormalized-keys.js +++ b/src/rules/no-unnormalized-keys.js @@ -15,7 +15,6 @@ import { getKey } from "../util.js"; /** * @import { JSONRuleDefinition } from "../types.ts"; - * * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds * @typedef {{ form: string }} NoUnnormalizedKeysOptions * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition diff --git a/src/rules/no-unsafe-values.js b/src/rules/no-unsafe-values.js index 355500d..5b38512 100644 --- a/src/rules/no-unsafe-values.js +++ b/src/rules/no-unsafe-values.js @@ -9,7 +9,6 @@ /** * @import { JSONRuleDefinition } from "../types.ts"; - * * @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds * @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition */ diff --git a/src/rules/sort-keys.js b/src/rules/sort-keys.js index 20dc797..fc1cc10 100644 --- a/src/rules/sort-keys.js +++ b/src/rules/sort-keys.js @@ -18,18 +18,20 @@ import { getKey, getRawKey } from "../util.js"; /** * @import { JSONRuleDefinition } from "../types.ts"; * @import { MemberNode } from "@humanwhocodes/momoa"; - * * @typedef {Object} SortOptions - * @property {boolean} caseSensitive - * @property {boolean} natural - * @property {number} minKeys - * @property {boolean} allowLineSeparatedGroups - * + * @property {boolean} caseSensitive Whether key comparisons are case-sensitive. + * @property {boolean} natural Whether to use natural sort order instead of purely alphanumeric. + * @property {number} minKeys Minimum number of keys in an object before enforcing sorting. + * @property {boolean} allowLineSeparatedGroups Whether a blank line between properties starts a new group that is independently sorted. * @typedef {"sortKeys"} SortKeysMessageIds * @typedef {"asc"|"desc"} SortDirection * @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions * @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition * @typedef {(a:string,b:string) => boolean} Comparator + * @typedef {"ascending"|"descending"} DirectionName + * @typedef {"alphanumeric"|"natural"} SortName + * @typedef {"sensitive"|"insensitive"} Sensitivity + * @typedef {Record>>} ComparatorMap */ //----------------------------------------------------------------------------- @@ -38,39 +40,30 @@ import { getKey, getRawKey } from "../util.js"; const hasNonWhitespace = /\S/u; +/** @type {ComparatorMap} */ const comparators = { ascending: { alphanumeric: { - /** @type {Comparator} */ sensitive: (a, b) => a <= b, - - /** @type {Comparator} */ insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(), }, natural: { - /** @type {Comparator} */ sensitive: (a, b) => naturalCompare(a, b) <= 0, - - /** @type {Comparator} */ insensitive: (a, b) => naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0, }, }, descending: { alphanumeric: { - /** @type {Comparator} */ sensitive: (a, b) => comparators.ascending.alphanumeric.sensitive(b, a), - /** @type {Comparator} */ insensitive: (a, b) => comparators.ascending.alphanumeric.insensitive(b, a), }, natural: { - /** @type {Comparator} */ sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a), - /** @type {Comparator} */ insensitive: (a, b) => comparators.ascending.natural.insensitive(b, a), }, @@ -140,9 +133,13 @@ const rule = { { allowLineSeparatedGroups, caseSensitive, natural, minKeys }, ] = context.options; + /** @type {DirectionName} */ const direction = directionShort === "asc" ? "ascending" : "descending"; + /** @type {SortName} */ const sortName = natural ? "natural" : "alphanumeric"; + /** @type {Sensitivity} */ const sensitivity = caseSensitive ? "sensitive" : "insensitive"; + /** @type {Comparator} */ const isValidOrder = comparators[direction][sortName][sensitivity]; // 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 @@ -161,7 +158,7 @@ const rule = { * Checks if two members are line-separated. * @param {MemberNode} prevMember The previous member. * @param {MemberNode} member The current member. - * @return {boolean} + * @returns {boolean} True if the members are separated by at least one blank line (ignoring comment-only lines). */ function isLineSeparated(prevMember, member) { // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups diff --git a/src/rules/top-level-interop.js b/src/rules/top-level-interop.js index 9d65953..579e735 100644 --- a/src/rules/top-level-interop.js +++ b/src/rules/top-level-interop.js @@ -9,7 +9,6 @@ /** * @import { JSONRuleDefinition } from "../types.ts"; - * * @typedef {"topLevel"} TopLevelInteropMessageIds * @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition */