first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
@@ -0,0 +1,14 @@
export default rule;
export type NoDuplicateKeysMessageIds = "duplicateKey";
export type NoDuplicateKeysRuleDefinition = JSONRuleDefinition<{
MessageIds: NoDuplicateKeysMessageIds;
}>;
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
*/
/** @type {NoDuplicateKeysRuleDefinition} */
declare const rule: NoDuplicateKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+68
View File
@@ -0,0 +1,68 @@
/**
* @fileoverview Rule to prevent duplicate keys in JSON.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MemberNode } from "@humanwhocodes/momoa";
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoDuplicateKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow duplicate keys in JSON objects",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
},
messages: {
duplicateKey: 'Duplicate key "{{key}}" found.',
},
},
create(context) {
/** @type {Array<Map<string, MemberNode>|undefined>} */
const objectKeys = [];
/** @type {Map<string, MemberNode>|undefined} */
let keys;
return {
Object() {
objectKeys.push(keys);
keys = new Map();
},
Member(node) {
const key = getKey(node);
const rawKey = getRawKey(node, context.sourceCode);
if (keys.has(key)) {
context.report({
loc: node.name.loc,
messageId: "duplicateKey",
data: {
key: rawKey,
},
});
}
else {
keys.set(key, node);
}
},
"Object:exit"() {
keys = objectKeys.pop();
},
};
},
};
export default rule;
+13
View File
@@ -0,0 +1,13 @@
export default rule;
export type NoEmptyKeysMessageIds = "emptyKey";
export type NoEmptyKeysRuleDefinition = JSONRuleDefinition<{
MessageIds: NoEmptyKeysMessageIds;
}>;
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"emptyKey"} NoEmptyKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
*/
/** @type {NoEmptyKeysRuleDefinition} */
declare const rule: NoEmptyKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+49
View File
@@ -0,0 +1,49 @@
/**
* @fileoverview Rule to prevent empty keys in JSON.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"emptyKey"} NoEmptyKeysMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoEmptyKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow empty keys in JSON objects",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
},
messages: {
emptyKey: "Empty key found.",
},
},
create(context) {
return {
Member(node) {
const key = getKey(node);
if (key.trim() === "") {
context.report({
loc: node.name.loc,
messageId: "emptyKey",
});
}
},
};
},
};
export default rule;
@@ -0,0 +1,18 @@
export default rule;
export type NoUnnormalizedKeysMessageIds = "unnormalizedKey";
export type NoUnnormalizedKeysOptions = {
form: string;
};
export type NoUnnormalizedKeysRuleDefinition = JSONRuleDefinition<{
RuleOptions: [NoUnnormalizedKeysOptions];
MessageIds: NoUnnormalizedKeysMessageIds;
}>;
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
* @typedef {{ form: string }} NoUnnormalizedKeysOptions
* @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
*/
/** @type {NoUnnormalizedKeysRuleDefinition} */
declare const rule: NoUnnormalizedKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
@@ -0,0 +1,83 @@
/**
* @fileoverview Rule to detect unnormalized keys in JSON.
* @author Bradley Meck Farias
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
* @typedef {{ form: string }} NoUnnormalizedKeysOptions
* @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoUnnormalizedKeysRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
fixable: "code",
docs: {
recommended: true,
description: "Disallow JSON keys that are not normalized",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
},
messages: {
unnormalizedKey: "Unnormalized key '{{key}}' found.",
},
schema: [
{
type: "object",
properties: {
form: {
enum: ["NFC", "NFD", "NFKC", "NFKD"],
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
form: "NFC",
},
],
},
create(context) {
const [{ form }] = context.options;
return {
Member(node) {
const key = getKey(node);
const rawKey = getRawKey(node, context.sourceCode);
const normalizedKey = key.normalize(form);
if (normalizedKey !== key) {
const { name } = node;
context.report({
loc: name.loc,
messageId: "unnormalizedKey",
data: {
key: rawKey,
},
fix(fixer) {
if (key !== rawKey) {
// Do not perform auto-fix when the raw key contains escape sequences.
return null;
}
return fixer.replaceTextRange(name.type === "String"
? [name.range[0] + 1, name.range[1] - 1]
: name.range, normalizedKey);
},
});
}
},
};
},
};
export default rule;
@@ -0,0 +1,8 @@
export default rule;
export type NoUnsafeValuesMessageIds = "unsafeNumber" | "unsafeInteger" | "unsafeZero" | "subnormal" | "loneSurrogate";
export type NoUnsafeValuesRuleDefinition = JSONRuleDefinition<{
MessageIds: NoUnsafeValuesMessageIds;
}>;
/** @type {NoUnsafeValuesRuleDefinition} */
declare const rule: NoUnsafeValuesRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+141
View File
@@ -0,0 +1,141 @@
/**
* @fileoverview Rule to detect unsafe values in JSON.
* @author Bradley Meck Farias
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/*
* This rule is based on the JSON grammar from RFC 8259, section 6.
* https://tools.ietf.org/html/rfc8259#section-6
*
* Also, this rule is based on the JSON5 grammar from json5.org, section 6.
* https://spec.json5.org/#numbers
*
* We separately capture the integer and fractional parts of a number, so that
* we can check for unsafe numbers that will evaluate to Infinity.
*/
const NUMBER = /^[+-]?(?<int>0|([1-9]\d*))?(?:\.(?<frac>\d*))?(?:e[+-]?\d+)?$/iu;
const NON_ZERO = /[1-9]/u;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoUnsafeValuesRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: true,
description: "Disallow JSON values that are unsafe for interchange",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
},
messages: {
unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
unsafeInteger: "The integer '{{ value }}' is outside the safe integer range.",
unsafeZero: "The number '{{ value }}' will evaluate to zero.",
subnormal: "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
},
},
create(context) {
return {
Number(node) {
const value = context.sourceCode.getText(node);
if (Number.isFinite(node.value) !== true) {
context.report({
loc: node.loc,
messageId: "unsafeNumber",
data: { value },
});
}
else {
// Also matches -0, intentionally
if (node.value === 0) {
// If the value has been rounded down to 0, but there was some
// fraction or non-zero part before the e-, this is a very small
// number that doesn't fit inside an f64.
const match = value.match(NUMBER);
if (match === null) {
return;
}
// If any part of the number other than the exponent has a
// non-zero digit in it, this number was not intended to be
// evaluated down to a zero.
if (NON_ZERO.test(match.groups.int) ||
NON_ZERO.test(match.groups.frac)) {
context.report({
loc: node.loc,
messageId: "unsafeZero",
data: { value },
});
}
}
else if (!/[.e]/iu.test(value)) {
// Intended to be an integer
if (node.value > Number.MAX_SAFE_INTEGER ||
node.value < Number.MIN_SAFE_INTEGER) {
context.report({
loc: node.loc,
messageId: "unsafeInteger",
data: { value },
});
}
}
else {
// Floating point. Check for subnormal.
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, node.value, false);
const asBigInt = view.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((asBigInt & 0x7ff0000000000000n) === 0n) {
context.report({
loc: node.loc,
messageId: "subnormal",
// Value included so that it's seen in scientific notation
data: {
value,
},
});
}
}
}
},
String(node) {
if (node.value.isWellFormed) {
if (node.value.isWellFormed()) {
return;
}
}
// match any high surrogate and, if it exists, a paired low surrogate
// match any low surrogate not already matched
const surrogatePattern = /[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu;
/** @type {RegExpExecArray | null} */
let match;
while ((match = surrogatePattern.exec(node.value)) !== null) {
// only need to report non-paired surrogates
if (match[0].length < 2) {
context.report({
loc: node.loc,
messageId: "loneSurrogate",
data: {
surrogate: JSON.stringify(match[0]).slice(1, -1),
},
});
}
}
},
};
},
};
export default rule;
+34
View File
@@ -0,0 +1,34 @@
export default rule;
export type SortOptions = {
/**
* Whether key comparisons are case-sensitive.
*/
caseSensitive: boolean;
/**
* Whether to use natural sort order instead of purely alphanumeric.
*/
natural: boolean;
/**
* Minimum number of keys in an object before enforcing sorting.
*/
minKeys: number;
/**
* Whether a blank line between properties starts a new group that is independently sorted.
*/
allowLineSeparatedGroups: boolean;
};
export type SortKeysMessageIds = "sortKeys";
export type SortDirection = "asc" | "desc";
export type SortKeysRuleOptions = [SortDirection, SortOptions];
export type SortKeysRuleDefinition = JSONRuleDefinition<{
RuleOptions: SortKeysRuleOptions;
MessageIds: SortKeysMessageIds;
}>;
export type Comparator = (a: string, b: string) => boolean;
export type DirectionName = "ascending" | "descending";
export type SortName = "alphanumeric" | "natural";
export type Sensitivity = "sensitive" | "insensitive";
export type ComparatorMap = Record<DirectionName, Record<SortName, Record<Sensitivity, Comparator>>>;
/** @type {SortKeysRuleDefinition} */
declare const rule: SortKeysRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+221
View File
@@ -0,0 +1,221 @@
/**
* @fileoverview Rule to require JSON object keys to be sorted.
* Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
* @author Robin Thomas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import naturalCompare from "natural-compare";
import { getKey, getRawKey } from "../util.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @import { MemberNode } from "@humanwhocodes/momoa";
* @typedef {Object} SortOptions
* @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<DirectionName, Record<SortName, Record<Sensitivity, Comparator>>>} ComparatorMap
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const hasNonWhitespace = /\S/u;
const commentTypes = new Set(["LineComment", "BlockComment"]);
/** @type {ComparatorMap} */
const comparators = {
ascending: {
alphanumeric: {
sensitive: (a, b) => a <= b,
insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
},
natural: {
sensitive: (a, b) => naturalCompare(a, b) <= 0,
insensitive: (a, b) => naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
},
},
descending: {
alphanumeric: {
sensitive: (a, b) => comparators.ascending.alphanumeric.sensitive(b, a),
insensitive: (a, b) => comparators.ascending.alphanumeric.insensitive(b, a),
},
natural: {
sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
insensitive: (a, b) => comparators.ascending.natural.insensitive(b, a),
},
},
};
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {SortKeysRuleDefinition} */
const rule = {
meta: {
type: "suggestion",
languages: ["json/json", "json/jsonc", "json/json5"],
fixable: "code",
defaultOptions: [
"asc",
{
allowLineSeparatedGroups: false,
caseSensitive: true,
minKeys: 2,
natural: false,
},
],
docs: {
recommended: false,
description: `Require JSON object keys to be sorted`,
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
},
messages: {
sortKeys: "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
},
schema: [
{
enum: ["asc", "desc"],
},
{
type: "object",
properties: {
caseSensitive: {
type: "boolean",
},
natural: {
type: "boolean",
},
minKeys: {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
create(context) {
const { sourceCode } = context;
const [directionShort, { 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
const commentLineNums = new Set();
for (const comment of sourceCode.comments) {
for (let lineNum = comment.loc.start.line; lineNum <= comment.loc.end.line; lineNum += 1) {
commentLineNums.add(lineNum);
}
}
/**
* Checks if two members are line-separated.
* @param {MemberNode} prevMember The previous member.
* @param {MemberNode} member The current member.
* @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
const prevMemberEndLine = prevMember.loc.end.line;
const thisStartLine = member.loc.start.line;
if (thisStartLine - prevMemberEndLine < 2) {
return false;
}
for (let lineNum = prevMemberEndLine + 1; lineNum < thisStartLine; lineNum += 1) {
if (!commentLineNums.has(lineNum) &&
!hasNonWhitespace.test(sourceCode.lines[lineNum - 1])) {
return true;
}
}
return false;
}
/**
* Checks if a member has a comment before or after it.
* @param {MemberNode} member The member to check.
* @returns {boolean} True if a comment is adjacent to the member.
*/
function hasAdjacentComment(member) {
const before = sourceCode.getTokenBefore(member, {
includeComments: true,
});
let after = sourceCode.getTokenAfter(member, {
includeComments: true,
});
if (after.type === "Comma") {
after = sourceCode.getTokenAfter(after, {
includeComments: true,
});
}
return (commentTypes.has(before.type) || commentTypes.has(after.type));
}
return {
Object(node) {
/** @type {MemberNode} */
let prevMember;
/** @type {string} */
let prevName;
/** @type {string} */
let prevRawName;
if (node.members.length < minKeys) {
return;
}
for (const member of node.members) {
const thisName = getKey(member);
const thisRawName = getRawKey(member, sourceCode);
// Capture `prevMember` for this iteration so the fixer closure uses the
// intended node even though `prevMember` is reassigned in the loop.
const prevMemberNode = prevMember;
if (prevMember &&
!isValidOrder(prevName, thisName) &&
(!allowLineSeparatedGroups ||
!isLineSeparated(prevMember, member))) {
context.report({
loc: member.name.loc,
messageId: "sortKeys",
data: {
thisName: thisRawName,
prevName: prevRawName,
direction,
sensitivity,
sortName,
},
fix(fixer) {
if (hasAdjacentComment(member) ||
hasAdjacentComment(prevMemberNode)) {
return null;
}
return [
fixer.replaceText(member, sourceCode.getText(prevMemberNode)),
fixer.replaceText(prevMemberNode, sourceCode.getText(member)),
];
},
});
}
prevMember = member;
prevName = thisName;
prevRawName = thisRawName;
}
},
};
},
};
export default rule;
@@ -0,0 +1,17 @@
export default rule;
export type TopLevelInteropMessageIds = "topLevel";
export type TopLevelInteropRuleDefinition = JSONRuleDefinition<{
MessageIds: TopLevelInteropMessageIds;
}>;
/**
* @fileoverview Rule to ensure top-level items are either an array or object.
* @author Joe Hildebrand
*/
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"topLevel"} TopLevelInteropMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
*/
/** @type {TopLevelInteropRuleDefinition} */
declare const rule: TopLevelInteropRuleDefinition;
import type { JSONRuleDefinition } from "../types.js";
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview Rule to ensure top-level items are either an array or object.
* @author Joe Hildebrand
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { JSONRuleDefinition } from "../types.js";
* @typedef {"topLevel"} TopLevelInteropMessageIds
* @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {TopLevelInteropRuleDefinition} */
const rule = {
meta: {
type: "problem",
languages: ["json/json", "json/jsonc", "json/json5"],
docs: {
recommended: false,
description: "Require the JSON top-level value to be an array or object",
dialects: ["JSON", "JSONC", "JSON5"],
url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
},
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 },
});
}
},
};
},
};
export default rule;