first commit
This commit is contained in:
+7
@@ -0,0 +1,7 @@
|
||||
export default rules;
|
||||
declare const rules: {
|
||||
readonly "json/no-duplicate-keys": "error";
|
||||
readonly "json/no-empty-keys": "error";
|
||||
readonly "json/no-unnormalized-keys": "error";
|
||||
readonly "json/no-unsafe-values": "error";
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
const rules = /** @type {const} */ ({
|
||||
"json/no-duplicate-keys": "error",
|
||||
"json/no-empty-keys": "error",
|
||||
"json/no-unnormalized-keys": "error",
|
||||
"json/no-unsafe-values": "error"
|
||||
});
|
||||
export default rules;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
declare const _default: {
|
||||
"no-duplicate-keys": import("../rules/no-duplicate-keys.js").NoDuplicateKeysRuleDefinition;
|
||||
"no-empty-keys": import("../rules/no-empty-keys.js").NoEmptyKeysRuleDefinition;
|
||||
"no-unnormalized-keys": import("../rules/no-unnormalized-keys.js").NoUnnormalizedKeysRuleDefinition;
|
||||
"no-unsafe-values": import("../rules/no-unsafe-values.js").NoUnsafeValuesRuleDefinition;
|
||||
"sort-keys": import("../rules/sort-keys.js").SortKeysRuleDefinition;
|
||||
"top-level-interop": import("../rules/top-level-interop.js").TopLevelInteropRuleDefinition;
|
||||
};
|
||||
export default _default;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import rule0 from "../rules/no-duplicate-keys.js";
|
||||
import rule1 from "../rules/no-empty-keys.js";
|
||||
import rule2 from "../rules/no-unnormalized-keys.js";
|
||||
import rule3 from "../rules/no-unsafe-values.js";
|
||||
import rule4 from "../rules/sort-keys.js";
|
||||
import rule5 from "../rules/top-level-interop.js";
|
||||
export default {
|
||||
"no-duplicate-keys": rule0,
|
||||
"no-empty-keys": rule1,
|
||||
"no-unnormalized-keys": rule2,
|
||||
"no-unsafe-values": rule3,
|
||||
"sort-keys": rule4,
|
||||
"top-level-interop": rule5,
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
export default plugin;
|
||||
export { JSONSourceCode };
|
||||
export * from "./languages/json-language.js";
|
||||
export * from "./types.js";
|
||||
declare namespace plugin {
|
||||
export namespace meta {
|
||||
let name: string;
|
||||
let namespace: string;
|
||||
let version: string;
|
||||
}
|
||||
export namespace languages {
|
||||
let json: JSONLanguage;
|
||||
let jsonc: JSONLanguage;
|
||||
let json5: JSONLanguage;
|
||||
}
|
||||
export { rules };
|
||||
export namespace configs {
|
||||
namespace recommended {
|
||||
let name_1: string;
|
||||
export { name_1 as name };
|
||||
export let plugins: {};
|
||||
export { recommendedRules as rules };
|
||||
}
|
||||
}
|
||||
}
|
||||
import { JSONSourceCode } from "./languages/json-source-code.js";
|
||||
import { JSONLanguage } from "./languages/json-language.js";
|
||||
import rules from "./build/rules.js";
|
||||
import recommendedRules from "./build/recommended-config.js";
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview JSON plugin.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { JSONLanguage } from "./languages/json-language.js";
|
||||
import { JSONSourceCode } from "./languages/json-source-code.js";
|
||||
import recommendedRules from "./build/recommended-config.js";
|
||||
import rules from "./build/rules.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Plugin
|
||||
//-----------------------------------------------------------------------------
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: "@eslint/json",
|
||||
namespace: "json",
|
||||
version: "2.0.1", // x-release-please-version
|
||||
},
|
||||
languages: {
|
||||
json: new JSONLanguage({ mode: "json" }),
|
||||
jsonc: new JSONLanguage({ mode: "jsonc" }),
|
||||
json5: new JSONLanguage({ mode: "json5" }),
|
||||
},
|
||||
rules,
|
||||
configs: {
|
||||
recommended: {
|
||||
name: "@eslint/json/recommended",
|
||||
plugins: {},
|
||||
rules: recommendedRules,
|
||||
},
|
||||
},
|
||||
};
|
||||
Object.assign(plugin.configs.recommended.plugins, { json: plugin });
|
||||
export default plugin;
|
||||
export { JSONSourceCode };
|
||||
export * from "./languages/json-language.js";
|
||||
export * from "./types.js";
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa";
|
||||
* @import { Language, OkParseResult, ParseResult, File } from "@eslint/core";
|
||||
* @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
|
||||
* @typedef {ParseResult<DocumentNode>} JSONParseResult
|
||||
* @typedef {Object} JSONLanguageOptions
|
||||
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
|
||||
*/
|
||||
/**
|
||||
* JSON Language Object
|
||||
* @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
|
||||
*/
|
||||
export class JSONLanguage implements Language {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options to use for this instance.
|
||||
* @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
|
||||
*/
|
||||
constructor({ mode }: {
|
||||
mode: "json" | "jsonc" | "json5";
|
||||
});
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType: "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart: 0 | 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart: 0 | 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey: string;
|
||||
/**
|
||||
* The visitor keys.
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
visitorKeys: Record<string, string[]>;
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {JSONLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {Error} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions: JSONLanguageOptions): void;
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
|
||||
* @returns {JSONParseResult} The result of parsing.
|
||||
*/
|
||||
parse(file: File, context: {
|
||||
languageOptions: JSONLanguageOptions;
|
||||
}): JSONParseResult;
|
||||
/**
|
||||
* Creates a new `JSONSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `JSONSourceCode` object from.
|
||||
* @param {JSONOkParseResult} parseResult The result returned from `parse()`.
|
||||
* @returns {JSONSourceCode} The new `JSONSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file: File, parseResult: JSONOkParseResult): JSONSourceCode;
|
||||
#private;
|
||||
}
|
||||
export type JSONOkParseResult = OkParseResult<DocumentNode>;
|
||||
export type JSONParseResult = ParseResult<DocumentNode>;
|
||||
export type JSONLanguageOptions = {
|
||||
/**
|
||||
* Whether to allow trailing commas in JSONC mode.
|
||||
*/
|
||||
allowTrailingCommas?: boolean;
|
||||
};
|
||||
import type { Language } from "@eslint/core";
|
||||
import type { File } from "@eslint/core";
|
||||
import { JSONSourceCode } from "./json-source-code.js";
|
||||
import type { DocumentNode } from "@humanwhocodes/momoa";
|
||||
import type { OkParseResult } from "@eslint/core";
|
||||
import type { ParseResult } from "@eslint/core";
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @fileoverview The JSONLanguage class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//------------------------------------------------------------------------------
|
||||
// Imports
|
||||
//------------------------------------------------------------------------------
|
||||
import { parse } from "@humanwhocodes/momoa";
|
||||
import { JSONSourceCode } from "./json-source-code.js";
|
||||
import { visitorKeys } from "@humanwhocodes/momoa";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { DocumentNode, AnyNode } from "@humanwhocodes/momoa";
|
||||
* @import { Language, OkParseResult, ParseResult, File } from "@eslint/core";
|
||||
* @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
|
||||
* @typedef {ParseResult<DocumentNode>} JSONParseResult
|
||||
* @typedef {Object} JSONLanguageOptions
|
||||
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* JSON Language Object
|
||||
* @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
|
||||
*/
|
||||
export class JSONLanguage {
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType = "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart = 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart = 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey = "type";
|
||||
/**
|
||||
* The parser mode.
|
||||
* @type {"json"|"jsonc"|"json5"}
|
||||
*/
|
||||
#mode = "json";
|
||||
/**
|
||||
* The visitor keys.
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
visitorKeys = Object.fromEntries([...visitorKeys]);
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options to use for this instance.
|
||||
* @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
|
||||
*/
|
||||
constructor({ mode }) {
|
||||
this.#mode = mode;
|
||||
}
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {JSONLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {Error} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions) {
|
||||
if (languageOptions.allowTrailingCommas !== undefined) {
|
||||
if (typeof languageOptions.allowTrailingCommas !== "boolean") {
|
||||
throw new Error("allowTrailingCommas must be a boolean if provided.");
|
||||
}
|
||||
// we know that allowTrailingCommas is a boolean here
|
||||
// only allowed in JSONC mode
|
||||
if (this.#mode !== "jsonc") {
|
||||
throw new Error("allowTrailingCommas option is only available in JSONC.");
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
|
||||
* @returns {JSONParseResult} The result of parsing.
|
||||
*/
|
||||
parse(file, context) {
|
||||
// Note: BOM already removed
|
||||
const text = /** @type {string} */ (file.body);
|
||||
const allowTrailingCommas = context?.languageOptions?.allowTrailingCommas;
|
||||
/*
|
||||
* Check for parsing errors first. If there's a parsing error, nothing
|
||||
* else can happen. However, a parsing error does not throw an error
|
||||
* from this method - it's just considered a fatal error message, a
|
||||
* problem that ESLint identified just like any other.
|
||||
*/
|
||||
try {
|
||||
const root = parse(text, {
|
||||
mode: this.#mode,
|
||||
ranges: true,
|
||||
tokens: true,
|
||||
allowTrailingCommas,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
ast: root,
|
||||
};
|
||||
}
|
||||
catch (ex) {
|
||||
// error messages end with (line:column) so we strip that off for ESLint
|
||||
const message = ex.message
|
||||
.slice(0, ex.message.lastIndexOf("("))
|
||||
.trim();
|
||||
return {
|
||||
ok: false,
|
||||
errors: [
|
||||
{
|
||||
...ex,
|
||||
message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
/* eslint-disable class-methods-use-this -- Required to complete interface. */
|
||||
/**
|
||||
* Creates a new `JSONSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `JSONSourceCode` object from.
|
||||
* @param {JSONOkParseResult} parseResult The result returned from `parse()`.
|
||||
* @returns {JSONSourceCode} The new `JSONSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file, parseResult) {
|
||||
return new JSONSourceCode({
|
||||
text: /** @type {string} */ (file.body),
|
||||
ast: parseResult.ast,
|
||||
});
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* JSON Source Code Object
|
||||
* @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
|
||||
*/
|
||||
export class JSONSourceCode extends TextSourceCodeBase<{
|
||||
LangOptions: JSONLanguageOptions;
|
||||
RootNode: DocumentNode;
|
||||
SyntaxElementWithLoc: JSONSyntaxElement;
|
||||
ConfigNode: Token;
|
||||
}> {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {DocumentNode} options.ast The root AST node.
|
||||
*/
|
||||
constructor({ text, ast }: {
|
||||
text: string;
|
||||
ast: DocumentNode;
|
||||
});
|
||||
/**
|
||||
* The comment tokens in the source code.
|
||||
* @type {Array<Token>|undefined}
|
||||
*/
|
||||
comments: Array<Token> | undefined;
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<Token>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes(): Array<Token>;
|
||||
/**
|
||||
* Returns directives that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives(): {
|
||||
problems: Array<FileProblem>;
|
||||
directives: Array<Directive>;
|
||||
};
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig(): {
|
||||
problems: Array<FileProblem>;
|
||||
configs: Array<{
|
||||
config: {
|
||||
rules: RulesConfig;
|
||||
};
|
||||
loc: LocationRange;
|
||||
}>;
|
||||
};
|
||||
/**
|
||||
* Returns the parent of the given node.
|
||||
* @param {AnyNode} node The node to get the parent of.
|
||||
* @returns {AnyNode|undefined} The parent of the node.
|
||||
*/
|
||||
getParent(node: AnyNode): AnyNode | undefined;
|
||||
/**
|
||||
* Traverse the source code and return the steps that were taken.
|
||||
* @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
|
||||
*/
|
||||
traverse(): Iterable<JSONTraversalStep>;
|
||||
/**
|
||||
* Gets the token before the given node or token, optionally including comments.
|
||||
* @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {boolean} [options.includeComments] If true, return comments when they are present.
|
||||
* @returns {Token|null} The previous token or comment, or null if there is none.
|
||||
*/
|
||||
getTokenBefore(nodeOrToken: AnyNode | Token, { includeComments }?: {
|
||||
includeComments?: boolean;
|
||||
}): Token | null;
|
||||
/**
|
||||
* Gets the token after the given node or token, skipping any comments unless includeComments is true.
|
||||
* @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {boolean} [options.includeComments=false] If true, return comments when they are present.
|
||||
* @returns {Token|null} The next token or comment, or null if there is none.
|
||||
*/
|
||||
getTokenAfter(nodeOrToken: AnyNode | Token, { includeComments }?: {
|
||||
includeComments?: boolean;
|
||||
}): Token | null;
|
||||
#private;
|
||||
}
|
||||
import type { JSONLanguageOptions } from "./json-language.js";
|
||||
import type { DocumentNode } from "@humanwhocodes/momoa";
|
||||
import type { JSONSyntaxElement } from "../types.js";
|
||||
import type { Token } from "@humanwhocodes/momoa";
|
||||
import { TextSourceCodeBase } from "@eslint/plugin-kit";
|
||||
import type { FileProblem } from "@eslint/core";
|
||||
import { Directive } from "@eslint/plugin-kit";
|
||||
import type { RulesConfig } from "@eslint/core";
|
||||
import type { LocationRange } from "@humanwhocodes/momoa";
|
||||
import type { AnyNode } from "@humanwhocodes/momoa";
|
||||
/**
|
||||
* A class to represent a step in the traversal process.
|
||||
*/
|
||||
declare class JSONTraversalStep extends VisitNodeStep {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the step.
|
||||
* @param {AnyNode} options.target The target of the step.
|
||||
* @param {1|2} options.phase The phase of the step.
|
||||
* @param {Array<any>} options.args The arguments of the step.
|
||||
*/
|
||||
constructor({ target, phase, args }: {
|
||||
target: AnyNode;
|
||||
phase: 1 | 2;
|
||||
args: Array<any>;
|
||||
});
|
||||
/**
|
||||
* The target of the step.
|
||||
* @type {AnyNode}
|
||||
*/
|
||||
target: AnyNode;
|
||||
}
|
||||
import { VisitNodeStep } from "@eslint/plugin-kit";
|
||||
export {};
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* @fileoverview The JSONSourceCode class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { iterator } from "@humanwhocodes/momoa";
|
||||
import { VisitNodeStep, TextSourceCodeBase, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { DocumentNode, AnyNode, Token, LocationRange } from "@humanwhocodes/momoa";
|
||||
* @import { FileProblem, DirectiveType, RulesConfig } from "@eslint/core";
|
||||
* @import { JSONSyntaxElement } from "../types.js";
|
||||
* @import { JSONLanguageOptions } from "./json-language.js";
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const commentParser = new ConfigCommentParser();
|
||||
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
|
||||
/**
|
||||
* A class to represent a step in the traversal process.
|
||||
*/
|
||||
class JSONTraversalStep extends VisitNodeStep {
|
||||
/**
|
||||
* The target of the step.
|
||||
* @type {AnyNode}
|
||||
*/
|
||||
target = undefined;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the step.
|
||||
* @param {AnyNode} options.target The target of the step.
|
||||
* @param {1|2} options.phase The phase of the step.
|
||||
* @param {Array<any>} options.args The arguments of the step.
|
||||
*/
|
||||
constructor({ target, phase, args }) {
|
||||
super({ target, phase, args });
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Processes tokens to extract comments and their starting tokens.
|
||||
* @param {Array<Token>} tokens The tokens to process.
|
||||
* @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
|
||||
* An object containing an array of comments, a map of starting token range to token index, and
|
||||
* a map of ending token range to token index.
|
||||
*/
|
||||
function processTokens(tokens) {
|
||||
/** @type {Array<Token>} */
|
||||
const comments = [];
|
||||
/** @type {Map<number, number>} */
|
||||
const starts = new Map();
|
||||
/** @type {Map<number, number>} */
|
||||
const ends = new Map();
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token.type.endsWith("Comment")) {
|
||||
comments.push(token);
|
||||
}
|
||||
starts.set(token.range[0], i);
|
||||
ends.set(token.range[1], i);
|
||||
}
|
||||
return { comments, starts, ends };
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* JSON Source Code Object
|
||||
* @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
|
||||
*/
|
||||
export class JSONSourceCode extends TextSourceCodeBase {
|
||||
/**
|
||||
* Cached traversal steps.
|
||||
* @type {Array<JSONTraversalStep>|undefined}
|
||||
*/
|
||||
#steps;
|
||||
/**
|
||||
* Cache of parent nodes.
|
||||
* @type {WeakMap<AnyNode, AnyNode>}
|
||||
*/
|
||||
#parents = new WeakMap();
|
||||
/**
|
||||
* Collection of inline configuration comments.
|
||||
* @type {Array<Token>}
|
||||
*/
|
||||
#inlineConfigComments;
|
||||
/**
|
||||
* The AST of the source code.
|
||||
* @type {DocumentNode}
|
||||
*/
|
||||
ast = undefined;
|
||||
/**
|
||||
* The comment tokens in the source code.
|
||||
* @type {Array<Token>|undefined}
|
||||
*/
|
||||
comments;
|
||||
/**
|
||||
* A map of token start positions to their corresponding index.
|
||||
* @type {Map<number, number>}
|
||||
*/
|
||||
#tokenStarts;
|
||||
/**
|
||||
* A map of token end positions to their corresponding index.
|
||||
* @type {Map<number, number>}
|
||||
*/
|
||||
#tokenEnds;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {DocumentNode} options.ast The root AST node.
|
||||
*/
|
||||
constructor({ text, ast }) {
|
||||
super({ text, ast, lineEndingPattern: /\r\n|[\r\n]/u });
|
||||
this.ast = ast;
|
||||
const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
|
||||
this.comments = comments;
|
||||
this.#tokenStarts = starts;
|
||||
this.#tokenEnds = ends;
|
||||
}
|
||||
/**
|
||||
* Returns the value of the given comment.
|
||||
* @param {Token} comment The comment to get the value of.
|
||||
* @returns {string} The value of the comment.
|
||||
* @throws {Error} When an unexpected comment type is passed.
|
||||
*/
|
||||
#getCommentValue(comment) {
|
||||
if (comment.type === "LineComment") {
|
||||
return this.getText(comment).slice(2); // strip leading `//`
|
||||
}
|
||||
if (comment.type === "BlockComment") {
|
||||
return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
|
||||
}
|
||||
throw new Error(`Unexpected comment type '${comment.type}'`);
|
||||
}
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<Token>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes() {
|
||||
if (!this.#inlineConfigComments) {
|
||||
this.#inlineConfigComments = this.comments.filter(comment => INLINE_CONFIG.test(this.#getCommentValue(comment)));
|
||||
}
|
||||
return this.#inlineConfigComments ?? [];
|
||||
}
|
||||
/**
|
||||
* Returns directives that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<Directive>} */
|
||||
const directives = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
const { label, value, justification } = commentParser.parseDirective(this.#getCommentValue(comment));
|
||||
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
|
||||
if (label === "eslint-disable-line" &&
|
||||
comment.loc.start.line !== comment.loc.end.line) {
|
||||
const message = `${label} comment should not span multiple lines.`;
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message,
|
||||
loc: comment.loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
switch (label) {
|
||||
case "eslint-disable":
|
||||
case "eslint-enable":
|
||||
case "eslint-disable-next-line":
|
||||
case "eslint-disable-line": {
|
||||
const directiveType = label.slice("eslint-".length);
|
||||
directives.push(new Directive({
|
||||
type: /** @type {DirectiveType} */ (directiveType),
|
||||
node: comment,
|
||||
value,
|
||||
justification,
|
||||
}));
|
||||
}
|
||||
// no default
|
||||
}
|
||||
});
|
||||
return { problems, directives };
|
||||
}
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<{config:{rules:RulesConfig},loc:LocationRange}>} */
|
||||
const configs = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
const { label, value } = commentParser.parseDirective(this.#getCommentValue(comment));
|
||||
if (label === "eslint") {
|
||||
const parseResult = commentParser.parseJSONLikeConfig(value);
|
||||
if (parseResult.ok) {
|
||||
configs.push({
|
||||
config: {
|
||||
rules: parseResult.config,
|
||||
},
|
||||
loc: comment.loc,
|
||||
});
|
||||
}
|
||||
else {
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message:
|
||||
/** @type {{ok: false, error: { message: string }}} */ (parseResult).error.message,
|
||||
loc: comment.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
configs,
|
||||
problems,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Returns the parent of the given node.
|
||||
* @param {AnyNode} node The node to get the parent of.
|
||||
* @returns {AnyNode|undefined} The parent of the node.
|
||||
*/
|
||||
getParent(node) {
|
||||
return this.#parents.get(node);
|
||||
}
|
||||
/**
|
||||
* Traverse the source code and return the steps that were taken.
|
||||
* @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
|
||||
*/
|
||||
traverse() {
|
||||
// Because the AST doesn't mutate, we can cache the steps
|
||||
if (this.#steps) {
|
||||
return this.#steps.values();
|
||||
}
|
||||
/** @type {Array<JSONTraversalStep>} */
|
||||
const steps = (this.#steps = []);
|
||||
for (const { node, parent, phase } of iterator(this.ast)) {
|
||||
if (parent) {
|
||||
this.#parents.set(
|
||||
/** @type {AnyNode} */ (node),
|
||||
/** @type {AnyNode} */ (parent));
|
||||
}
|
||||
steps.push(new JSONTraversalStep({
|
||||
target: /** @type {AnyNode} */ (node),
|
||||
phase: phase === "enter" ? 1 : 2,
|
||||
args: [node, parent],
|
||||
}));
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
/**
|
||||
* Gets the token before the given node or token, optionally including comments.
|
||||
* @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {boolean} [options.includeComments] If true, return comments when they are present.
|
||||
* @returns {Token|null} The previous token or comment, or null if there is none.
|
||||
*/
|
||||
getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
|
||||
const index = this.#tokenStarts.get(nodeOrToken.range[0]);
|
||||
if (index === undefined) {
|
||||
return null;
|
||||
}
|
||||
let previousIndex = index - 1;
|
||||
if (previousIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
const tokens = this.ast.tokens;
|
||||
let tokenOrComment = tokens[previousIndex];
|
||||
if (includeComments) {
|
||||
return tokenOrComment;
|
||||
}
|
||||
// skip comments
|
||||
while (tokenOrComment?.type.endsWith("Comment")) {
|
||||
previousIndex--;
|
||||
if (previousIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
tokenOrComment = tokens[previousIndex];
|
||||
}
|
||||
return tokenOrComment;
|
||||
}
|
||||
/**
|
||||
* Gets the token after the given node or token, skipping any comments unless includeComments is true.
|
||||
* @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
|
||||
* @param {Object} [options] Options object.
|
||||
* @param {boolean} [options.includeComments=false] If true, return comments when they are present.
|
||||
* @returns {Token|null} The next token or comment, or null if there is none.
|
||||
*/
|
||||
getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
|
||||
const index = this.#tokenEnds.get(nodeOrToken.range[1]);
|
||||
if (index === undefined) {
|
||||
return null;
|
||||
}
|
||||
let nextIndex = index + 1;
|
||||
const tokens = this.ast.tokens;
|
||||
if (nextIndex >= tokens.length) {
|
||||
return null;
|
||||
}
|
||||
let tokenOrComment = tokens[nextIndex];
|
||||
if (includeComments) {
|
||||
return tokenOrComment;
|
||||
}
|
||||
// skip comments
|
||||
while (tokenOrComment?.type.endsWith("Comment")) {
|
||||
nextIndex++;
|
||||
if (nextIndex >= tokens.length) {
|
||||
return null;
|
||||
}
|
||||
tokenOrComment = tokens[nextIndex];
|
||||
}
|
||||
return tokenOrComment;
|
||||
}
|
||||
}
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+18
@@ -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";
|
||||
+83
@@ -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;
|
||||
+8
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+17
@@ -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
@@ -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;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @fileoverview Additional types for this package.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
import type { RuleVisitor } from "@eslint/core";
|
||||
import type { CustomRuleDefinitionType, CustomRuleTypeDefinitions, CustomRuleVisitorWithExit } from "@eslint/plugin-kit";
|
||||
import type { DocumentNode, MemberNode, ElementNode, ObjectNode, ArrayNode, StringNode, NullNode, NumberNode, BooleanNode, NaNNode, InfinityNode, IdentifierNode, AnyNode, Token } from "@humanwhocodes/momoa";
|
||||
import type { JSONLanguageOptions, JSONSourceCode } from "./index.js";
|
||||
type ValueNodeParent = DocumentNode | MemberNode | ElementNode;
|
||||
/**
|
||||
* A JSON syntax element, including nodes and tokens.
|
||||
*/
|
||||
export type JSONSyntaxElement = Token | AnyNode;
|
||||
/**
|
||||
* The visitor format returned from rules in this package.
|
||||
*/
|
||||
export interface JSONRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
||||
Document?(node: DocumentNode): void;
|
||||
Member?(node: MemberNode, parent?: ObjectNode): void;
|
||||
Element?(node: ElementNode, parent?: ArrayNode): void;
|
||||
Object?(node: ObjectNode, parent?: ValueNodeParent): void;
|
||||
Array?(node: ArrayNode, parent?: ValueNodeParent): void;
|
||||
String?(node: StringNode, parent?: ValueNodeParent): void;
|
||||
Null?(node: NullNode, parent?: ValueNodeParent): void;
|
||||
Number?(node: NumberNode, parent?: ValueNodeParent): void;
|
||||
Boolean?(node: BooleanNode, parent?: ValueNodeParent): void;
|
||||
NaN?(node: NaNNode, parent?: ValueNodeParent): void;
|
||||
Infinity?(node: InfinityNode, parent?: ValueNodeParent): void;
|
||||
Identifier?(node: IdentifierNode, parent?: ValueNodeParent): void;
|
||||
}> {
|
||||
}
|
||||
export type JSONRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
|
||||
export type JSONRuleDefinition<Options extends Partial<JSONRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
|
||||
LangOptions: JSONLanguageOptions;
|
||||
Code: JSONSourceCode;
|
||||
Visitor: JSONRuleVisitor;
|
||||
Node: JSONSyntaxElement;
|
||||
}, Options>;
|
||||
export {};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Additional types for this package.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
export {};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @fileoverview Utility Library
|
||||
* @author 루밀LuMir(lumirlumir)
|
||||
*/
|
||||
/**
|
||||
* @import { MemberNode } from "@humanwhocodes/momoa";
|
||||
* @import { JSONSourceCode } from "./languages/json-source-code.js";
|
||||
*/
|
||||
/**
|
||||
* 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: MemberNode): string;
|
||||
/**
|
||||
* 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: MemberNode, sourceCode: JSONSourceCode): string;
|
||||
import type { MemberNode } from "@humanwhocodes/momoa";
|
||||
import type { JSONSourceCode } from "./languages/json-source-code.js";
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
Reference in New Issue
Block a user