diff --git a/jsr.json b/jsr.json index ff3497d..d24bb93 100644 --- a/jsr.json +++ b/jsr.json @@ -6,6 +6,7 @@ "include": [ "dist/esm/index.js", "dist/esm/index.d.ts", + "dist/esm/types.ts", "README.md", "jsr.json", "LICENSE" diff --git a/package.json b/package.json index 1c4fb25..58ed0d7 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "homepage": "https://github.com/eslint/json#readme", "scripts": { "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js", - "build:cts": "node -e \"fs.copyFileSync('dist/esm/index.d.ts', 'dist/cjs/index.d.cts')\"", + "build:cts": "node tools/build-cts.js", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts", "build:readme": "node tools/update-readme.js", "test:jsr": "npx jsr@latest publish --dry-run", @@ -68,7 +68,6 @@ "@humanwhocodes/momoa": "^3.3.4" }, "devDependencies": { - "@types/eslint": "^8.56.10", "c8": "^9.1.0", "dedent": "^1.5.3", "eslint": "^9.11.1", diff --git a/rollup.config.js b/rollup.config.js index 0ec27fb..5b813a8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,5 @@ +import copy from "rollup-plugin-copy"; + export default { input: "src/index.js", output: [ @@ -11,4 +13,12 @@ export default { banner: '// @ts-self-types="./index.d.ts"', }, ], + plugins: [ + copy({ + targets: [ + { src: "src/types.ts", dest: "dist/cjs", rename: "types.cts" }, + { src: "src/types.ts", dest: "dist/esm" }, + ], + }), + ], }; diff --git a/src/languages/json-language.js b/src/languages/json-language.js index 9948aac..5a719af 100644 --- a/src/languages/json-language.js +++ b/src/languages/json-language.js @@ -21,10 +21,8 @@ import { visitorKeys } from "@humanwhocodes/momoa"; /** @typedef {import("@eslint/core").OkParseResult} OkParseResult */ /** @typedef {import("@eslint/core").ParseResult} ParseResult */ /** @typedef {import("@eslint/core").File} File */ -/** - * @typedef {Object} JSONLanguageOptions - * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas. - */ +/** @typedef {import("../types.ts").IJSONLanguage} IJSONLanguage */ +/** @typedef {import("../types.ts").JSONLanguageOptions} JSONLanguageOptions */ //----------------------------------------------------------------------------- // Exports @@ -32,7 +30,7 @@ import { visitorKeys } from "@humanwhocodes/momoa"; /** * JSON Language Object - * @implements {Language} + * @implements {IJSONLanguage} */ export class JSONLanguage { /** diff --git a/src/languages/json-source-code.js b/src/languages/json-source-code.js index acec0ec..e512f83 100644 --- a/src/languages/json-source-code.js +++ b/src/languages/json-source-code.js @@ -26,11 +26,12 @@ import { /** @typedef {import("@eslint/core").SourceLocation} SourceLocation */ /** @typedef {import("@eslint/core").File} File */ /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */ -/** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */ /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */ /** @typedef {import("@eslint/core").FileProblem} FileProblem */ /** @typedef {import("@eslint/core").DirectiveType} DirectiveType */ /** @typedef {import("@eslint/core").RulesConfig} RulesConfig */ +/** @typedef {import("../types.ts").IJSONSourceCode} IJSONSourceCode */ +/** @typedef {import("../types.ts").JSONSyntaxElement} JSONSyntaxElement */ //----------------------------------------------------------------------------- // Helpers @@ -71,6 +72,7 @@ class JSONTraversalStep extends VisitNodeStep { /** * JSON Source Code Object + * @implements {IJSONSourceCode} */ export class JSONSourceCode extends TextSourceCodeBase { /** @@ -147,7 +149,7 @@ export class JSONSourceCode extends TextSourceCodeBase { ); } - return this.#inlineConfigComments; + return this.#inlineConfigComments ?? []; } /** diff --git a/src/rules/no-duplicate-keys.js b/src/rules/no-duplicate-keys.js index 3499526..e3ac3d6 100644 --- a/src/rules/no-duplicate-keys.js +++ b/src/rules/no-duplicate-keys.js @@ -3,13 +3,22 @@ * @author Nicholas C. Zakas */ +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {"duplicateKey"} NoDuplicateKeysMessageIds */ +/** @typedef {import("../types.ts").JSONRuleDefinition<[], NoDuplicateKeysMessageIds>} NoDuplicateKeysRuleDefinition */ +/** @typedef {import("@humanwhocodes/momoa").MemberNode} MemberNode */ + //----------------------------------------------------------------------------- // Rule Definition //----------------------------------------------------------------------------- +/** @type {NoDuplicateKeysRuleDefinition} */ export default { meta: { - type: /** @type {const} */ ("problem"), + type: "problem", docs: { description: "Disallow duplicate keys in JSON objects", @@ -21,7 +30,10 @@ export default { }, create(context) { + /** @type {Array|undefined>} */ const objectKeys = []; + + /** @type {Map|undefined} */ let keys; return { diff --git a/src/rules/no-empty-keys.js b/src/rules/no-empty-keys.js index 3fedc4f..73983cc 100644 --- a/src/rules/no-empty-keys.js +++ b/src/rules/no-empty-keys.js @@ -3,9 +3,21 @@ * @author Nicholas C. Zakas */ +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {"emptyKey"} NoEmptyKeysMessageIds */ +/** @typedef {import("../types.ts").JSONRuleDefinition<[], NoEmptyKeysMessageIds>} NoEmptyKeysRuleDefinition */ + +//----------------------------------------------------------------------------- +// Rule Definition +//----------------------------------------------------------------------------- + +/** @type {NoEmptyKeysRuleDefinition} */ export default { meta: { - type: /** @type {const} */ ("problem"), + type: "problem", docs: { description: "Disallow empty keys in JSON objects", diff --git a/src/rules/no-unsafe-values.js b/src/rules/no-unsafe-values.js index ed29a48..6249e23 100644 --- a/src/rules/no-unsafe-values.js +++ b/src/rules/no-unsafe-values.js @@ -3,15 +3,36 @@ * @author Bradley Meck Farias */ -// RFC 8259's `number` production, as a regex. Capture the integer part -// and the fractional part. +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds */ +/** @typedef {import("../types.ts").JSONRuleDefinition<[], NoUnsafeValuesMessageIds>} NoUnsafeValuesRuleDefinition */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/* + * This rule is based on the JSON grammar from RFC 8259, section 6. + * https://tools.ietf.org/html/rfc8259#section-6 + * + * 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 = /^-?(?0|([1-9][0-9]*))(?:\.(?[0-9]+))?(?:[eE][+-]?[0-9]+)?$/u; const NON_ZERO = /[1-9]/u; +//----------------------------------------------------------------------------- +// Rule Definition +//----------------------------------------------------------------------------- + +/** @type {NoUnsafeValuesRuleDefinition} */ export default { meta: { - type: /** @type {const} */ ("problem"), + type: "problem", docs: { description: "Disallow JSON values that are unsafe for interchange", @@ -85,7 +106,9 @@ export default { loc: node.loc, messageId: "subnormal", // Value included so that it's seen in scientific notation - data: node, + data: { + value, + }, }); } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9c08e8b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,128 @@ +/** + * @fileoverview Additional types for this package. + * @author Nicholas C. Zakas + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import type { + RuleVisitor, + TextSourceCode, + Language, + LanguageOptions, + RuleDefinition, +} from "@eslint/core"; +import { + DocumentNode, + MemberNode, + ElementNode, + ObjectNode, + ArrayNode, + StringNode, + NullNode, + NumberNode, + BooleanNode, + NaNNode, + InfinityNode, + IdentifierNode, + AnyNode, + Token, +} from "@humanwhocodes/momoa"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +type ValueNodeParent = DocumentNode | MemberNode | ElementNode; + +/** + * A JSON syntax element, including nodes and tokens. + */ +export type JSONSyntaxElement = Token | AnyNode; + +/** + * Language options provided for JSON files. + */ +export interface JSONLanguageOptions extends LanguageOptions { + /** + * Whether to allow trailing commas. Only valid in JSONC. + */ + allowTrailingCommas?: boolean; +} + +/** + * The visitor format returned from rules in this package. + */ +export interface JSONRuleVisitor extends RuleVisitor { + 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; + + "Document:exit"?(node: DocumentNode): void; + "Member:exit"?(node: MemberNode, parent?: ObjectNode): void; + "Element:exit"?(node: ElementNode, parent?: ArrayNode): void; + "Object:exit"?(node: ObjectNode, parent?: ValueNodeParent): void; + "Array:exit"?(node: ArrayNode, parent?: ValueNodeParent): void; + "String:exit"?(node: StringNode, parent?: ValueNodeParent): void; + "Null:exit"?(node: NullNode, parent?: ValueNodeParent): void; + "Number:exit"?(node: NumberNode, parent?: ValueNodeParent): void; + "Boolean:exit"?(node: BooleanNode, parent?: ValueNodeParent): void; + "NaN:exit"?(node: NaNNode, parent?: ValueNodeParent): void; + "Infinity:exit"?(node: InfinityNode, parent?: ValueNodeParent): void; + "Identifier:exit"?(node: IdentifierNode, parent?: ValueNodeParent): void; +} + +/** + * The `SourceCode` implementation for JSON files. + */ +export interface IJSONSourceCode + extends TextSourceCode<{ + LangOptions: JSONLanguageOptions; + RootNode: DocumentNode; + SyntaxElementWithLoc: JSONSyntaxElement; + ConfigNode: Token; + }> { + /** + * Get the text of a syntax element. + * @param syntaxElement The syntax element to get the text of. + * @param beforeCount The number of characters to include before the syntax element. + * @param afterCount The number of characters to include after the syntax element. + * @returns The text of the syntax element. + */ + getText( + syntaxElement: JSONSyntaxElement, + beforeCount?: number, + afterCount?: number, + ): string; +} + +export type IJSONLanguage = Language<{ + LangOptions: JSONLanguageOptions; + Code: IJSONSourceCode; + RootNode: DocumentNode; + Node: AnyNode; +}>; + +export type JSONRuleDefinition< + JSONRuleOptions extends unknown[], + JSONRuleMessageIds extends string = "", +> = RuleDefinition<{ + LangOptions: JSONLanguageOptions; + Code: IJSONSourceCode; + RuleOptions: JSONRuleOptions; + Visitor: JSONRuleVisitor; + Node: AnyNode; + MessageIds: JSONRuleMessageIds; + ExtRuleDocs: {}; +}>; diff --git a/tests/rules/no-unsafe-values.test.js b/tests/rules/no-unsafe-values.test.js index 7e8234b..4a75c85 100644 --- a/tests/rules/no-unsafe-values.test.js +++ b/tests/rules/no-unsafe-values.test.js @@ -251,7 +251,7 @@ ruleTester.run("no-unsafe-values", rule, { { messageId: "subnormal", data: { - value: "2.225073858507201e-308", + value: "2.2250738585072009e-308", }, line: 1, column: 1, @@ -266,7 +266,7 @@ ruleTester.run("no-unsafe-values", rule, { { messageId: "subnormal", data: { - value: "-2.225073858507201e-308", + value: "-2.2250738585072009e-308", }, line: 1, column: 1, diff --git a/tests/types/types.test.ts b/tests/types/types.test.ts index a9f5374..6e6edd0 100644 --- a/tests/types/types.test.ts +++ b/tests/types/types.test.ts @@ -1,7 +1,7 @@ import json from "@eslint/json"; -import { ESLint } from "eslint"; +// import { ESLint } from "eslint"; -json satisfies ESLint.Plugin; +// json satisfies ESLint.Plugin; json.meta.name satisfies string; json.meta.version satisfies string; diff --git a/tools/build-cts.js b/tools/build-cts.js new file mode 100644 index 0000000..ade7ae1 --- /dev/null +++ b/tools/build-cts.js @@ -0,0 +1,16 @@ +/** + * @fileoverview Rewrites import expressions for CommonJS compatibility. + * This script creates "dist/cjs/index.d.cts" from "dist/esm/index.d.ts" by modifying imports + * from `"./types.ts"` to `"./types.cts"`. + * + * @author Francesco Trotta + */ + +import { readFile, writeFile } from "node:fs/promises"; + +const oldSourceText = await readFile("dist/esm/index.d.ts", "utf-8"); +const newSourceText = oldSourceText.replaceAll( + 'import("./types.ts")', + 'import("./types.cts")', +); +await writeFile("dist/cjs/index.d.cts", newSourceText); diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js index ce8b16d..b4a4ada 100644 --- a/tools/dedupe-types.js +++ b/tools/dedupe-types.js @@ -39,5 +39,10 @@ files.forEach(filePath => { return true; }); - fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); + // replace references to ../types.ts with ./types.ts + const text = remainingLines + .join("\n") + .replace(/\.\.\/types\.ts/gu, "./types.ts"); + + fs.writeFileSync(filePath, text, "utf8"); }); diff --git a/tsconfig.json b/tsconfig.json index 3fa504c..2e82117 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,9 @@ "allowJs": true, "checkJs": true, "outDir": "dist/esm", - "target": "ES2022", + "target": "ESNext", "moduleResolution": "NodeNext", - "module": "NodeNext" + "module": "NodeNext", + "allowImportingTsExtensions": true } }