diff --git a/package.json b/package.json index 52c7b9b..b9a9e1e 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "prettier": "@jimmy.codes/prettier-config", "dependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", "@eslint/js": "^9.14.0", "@tanstack/eslint-plugin-query": "^5.59.7", "@types/eslint": "9.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf53a17..2f7b767 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@eslint-community/eslint-plugin-eslint-comments': + specifier: ^4.4.1 + version: 4.4.1(eslint@9.13.0(jiti@2.4.0)) '@eslint/js': specifier: ^9.14.0 version: 9.14.0 @@ -794,6 +797,12 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': + resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5471,6 +5480,12 @@ snapshots: '@esbuild/win32-x64@0.24.0': optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.13.0(jiti@2.4.0))': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.13.0(jiti@2.4.0) + ignore: 5.3.2 + '@eslint-community/eslint-utils@4.4.1(eslint@9.13.0(jiti@2.4.0))': dependencies: eslint: 9.13.0(jiti@2.4.0) diff --git a/src/configs/eslint-comments.ts b/src/configs/eslint-comments.ts new file mode 100644 index 0000000..e796024 --- /dev/null +++ b/src/configs/eslint-comments.ts @@ -0,0 +1,15 @@ +import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; + +import { eslintCommentsRules } from "../rules/eslint-comments"; + +const eslintCommentsConfig = () => { + return [ + { + ...comments.recommended, + name: "jimmy.codes/eslint-comments", + rules: eslintCommentsRules, + }, + ]; +}; + +export default eslintCommentsConfig; diff --git a/src/factory.spec.ts b/src/factory.spec.ts index 78fc577..eba9f1d 100644 --- a/src/factory.spec.ts +++ b/src/factory.spec.ts @@ -6,7 +6,7 @@ vi.mock("local-pkg"); describe("jimmyDotCodes", () => { describe("base", () => { - it.each(["node", "imports", "perfectionist", "unicorn"])( + it.each(["node", "imports", "perfectionist", "unicorn", "eslint-comments"])( "should create configuration w/ %s", (input) => { expect(jimmyDotCodes({ autoDetect: false })).toStrictEqual( @@ -17,6 +17,7 @@ describe("jimmyDotCodes", () => { }, ); }); + it("should create configuration w/ typescript", () => { expect( jimmyDotCodes({ autoDetect: false, typescript: true }), @@ -179,7 +180,7 @@ describe("jimmyDotCodes", () => { it("should include react-query when auto detection is enabled", () => { vi.mocked(isPackageExists).mockImplementation((name) => { - // eslint-disable-next-line jest/no-conditional-in-test + // eslint-disable-next-line jest/no-conditional-in-test -- this condition is only for the mock. return name === "react" || name === "@tanstack/react-query"; }); @@ -240,7 +241,7 @@ describe("jimmyDotCodes", () => { it("should include test-library when auto detection is enabled", () => { vi.mocked(isPackageExists).mockImplementation((name) => { - // eslint-disable-next-line jest/no-conditional-in-test + // eslint-disable-next-line jest/no-conditional-in-test -- this condition is only for the mock. return name === "@testing-library/react" || name === "vitest"; }); diff --git a/src/factory.ts b/src/factory.ts index 383075f..63a27a0 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -6,6 +6,7 @@ import type { Options, TypedConfigItem } from "./types"; import { astroConfig } from "./configs/astro"; import { commonjsConfig } from "./configs/commonjs"; +import eslintCommentsConfig from "./configs/eslint-comments"; import importsConfig from "./configs/imports"; import nodeConfig from "./configs/node"; import perfectionistConfig from "./configs/perfectionist"; @@ -44,6 +45,7 @@ export const jimmyDotCodes = ( ...perfectionistConfig(), ...nodeConfig(), ...unicornConfig(), + ...eslintCommentsConfig(), ...importsConfig({ typescript: isTypescriptEnabled }), ...(isTypescriptEnabled ? typescriptConfig(getTypescriptOptions(typescript)) diff --git a/src/rules.gen.d.ts b/src/rules.gen.d.ts index b650bd2..694503a 100644 --- a/src/rules.gen.d.ts +++ b/src/rules.gen.d.ts @@ -3,6 +3,57 @@ import type { Linter } from 'eslint' export interface RuleOptions { + /** + * require a `eslint-enable` comment for every `eslint-disable` comment + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/disable-enable-pair.html + */ + "@eslint-community/eslint-comments/disable-enable-pair"?: Linter.RuleEntry; + /** + * disallow a `eslint-enable` comment for multiple `eslint-disable` comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-aggregating-enable.html + */ + "@eslint-community/eslint-comments/no-aggregating-enable"?: Linter.RuleEntry< + [] + >; + /** + * disallow duplicate `eslint-disable` comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-duplicate-disable.html + */ + "@eslint-community/eslint-comments/no-duplicate-disable"?: Linter.RuleEntry< + [] + >; + /** + * disallow `eslint-disable` comments about specific rules + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-restricted-disable.html + */ + "@eslint-community/eslint-comments/no-restricted-disable"?: Linter.RuleEntry; + /** + * disallow `eslint-disable` comments without rule names + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-unlimited-disable.html + */ + "@eslint-community/eslint-comments/no-unlimited-disable"?: Linter.RuleEntry< + [] + >; + /** + * disallow unused `eslint-disable` comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-unused-disable.html + */ + "@eslint-community/eslint-comments/no-unused-disable"?: Linter.RuleEntry<[]>; + /** + * disallow unused `eslint-enable` comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-unused-enable.html + */ + "@eslint-community/eslint-comments/no-unused-enable"?: Linter.RuleEntry<[]>; + /** + * disallow ESLint directive-comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-use.html + */ + "@eslint-community/eslint-comments/no-use"?: Linter.RuleEntry; + /** + * require include descriptions in ESLint directive-comments + * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/require-description.html + */ + "@eslint-community/eslint-comments/require-description"?: Linter.RuleEntry; /** * Exhaustive deps rule for useQuery * @see https://tanstack.com/query/latest/docs/eslint/exhaustive-deps @@ -4952,6 +5003,52 @@ export interface RuleOptions { } /* ======= Declarations ======= */ +// ----- @eslint-community/eslint-comments/disable-enable-pair ----- +type EslintCommunityEslintCommentsDisableEnablePair = + | [] + | [ + { + allowWholeFile?: boolean; + }, + ]; +// ----- @eslint-community/eslint-comments/no-restricted-disable ----- +type EslintCommunityEslintCommentsNoRestrictedDisable = string[]; +// ----- @eslint-community/eslint-comments/no-use ----- +type EslintCommunityEslintCommentsNoUse = + | [] + | [ + { + allow?: ( + | "eslint" + | "eslint-disable" + | "eslint-disable-line" + | "eslint-disable-next-line" + | "eslint-enable" + | "eslint-env" + | "exported" + | "global" + | "globals" + )[]; + }, + ]; +// ----- @eslint-community/eslint-comments/require-description ----- +type EslintCommunityEslintCommentsRequireDescription = + | [] + | [ + { + ignore?: ( + | "eslint" + | "eslint-disable" + | "eslint-disable-line" + | "eslint-disable-next-line" + | "eslint-enable" + | "eslint-env" + | "exported" + | "global" + | "globals" + )[]; + }, + ]; // ----- @typescript-eslint/array-type ----- type TypescriptEslintArrayType = | [] diff --git a/src/rules/__snapshots__/eslint-comments.spec.ts.snap b/src/rules/__snapshots__/eslint-comments.spec.ts.snap new file mode 100644 index 0000000..d46166b --- /dev/null +++ b/src/rules/__snapshots__/eslint-comments.spec.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should create eslintComments rules 1`] = ` +{ + "@eslint-community/eslint-comments/disable-enable-pair": "error", + "@eslint-community/eslint-comments/no-aggregating-enable": "error", + "@eslint-community/eslint-comments/no-duplicate-disable": "error", + "@eslint-community/eslint-comments/no-unlimited-disable": "error", + "@eslint-community/eslint-comments/no-unused-enable": "error", + "@eslint-community/eslint-comments/require-description": "error", +} +`; diff --git a/src/rules/eslint-comments.spec.ts b/src/rules/eslint-comments.spec.ts new file mode 100644 index 0000000..551b60f --- /dev/null +++ b/src/rules/eslint-comments.spec.ts @@ -0,0 +1,5 @@ +import { eslintCommentsRules } from "./eslint-comments"; + +test("should create eslintComments rules", () => { + expect(eslintCommentsRules).toMatchSnapshot(); +}); diff --git a/src/rules/eslint-comments.ts b/src/rules/eslint-comments.ts new file mode 100644 index 0000000..acccdd3 --- /dev/null +++ b/src/rules/eslint-comments.ts @@ -0,0 +1,8 @@ +import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; + +import type { Rules } from "../types"; + +export const eslintCommentsRules = { + ...comments.recommended.rules, + "@eslint-community/eslint-comments/require-description": "error", +} satisfies Rules; diff --git a/src/stubs.d.ts b/src/stubs.d.ts index 27193d8..6e80ab3 100644 --- a/src/stubs.d.ts +++ b/src/stubs.d.ts @@ -1,10 +1,8 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports */ - declare module "eslint-config-prettier" { - type Rules = import("eslint").Linter.RulesRecord; + import type { Linter } from "eslint"; interface Config { - rules: Rules; + rules: Linter.RulesRecord; } declare const config: Config; @@ -13,107 +11,103 @@ declare module "eslint-config-prettier" { } declare module "eslint-plugin-react-hooks" { - type Plugin = import("eslint").ESLint.Plugin; - type Rules = import("eslint").Linter.RulesRecord; + import type { ESLint, Linter } from "eslint"; - const plugin: Plugin; + const plugin: ESLint.Plugin; interface Configs { recommended: { plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; } export const configs: Configs; - - export const rules: Plugin["rules"]; + export const rules: ESLint.Plugin["rules"]; } declare module "eslint-plugin-jsx-a11y" { - type Plugin = import("eslint").ESLint.Plugin; - type Rules = import("eslint").Linter.RulesRecord; - - const plugin: Plugin; + import type { ESLint, Linter } from "eslint"; interface Configs { recommended: { plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; } export const configs: Configs; - - export const rules: Plugin["rules"]; + export const rules: ESLint.Plugin["rules"]; } declare module "eslint-plugin-jest" { - type Plugin = import("eslint").ESLint.Plugin; - type Rules = import("eslint").Linter.RulesRecord; - type LanguageOptions = import("eslint").Linter.Config["languageOptions"]; + import type { ESLint, Linter } from "eslint"; - const plugin: Plugin; + type LanguageOptions = Linter.Config["languageOptions"]; interface Configs { "flat/recommended": { languageOptions: LanguageOptions; plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; "flat/style": { languageOptions: LanguageOptions; plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; } export const configs: Configs; - - export const rules: Plugin["rules"]; + export const rules: ESLint.Plugin["rules"]; } declare module "eslint-plugin-testing-library" { - type Plugin = import("eslint").ESLint.Plugin; - type Rules = import("eslint").Linter.RulesRecord; - - const plugin: Plugin; + import type { ESLint, Linter } from "eslint"; interface Configs { react: { plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; } export const configs: Configs; - - export const rules: Plugin["rules"]; + export const rules: ESLint.Plugin["rules"]; } declare module "eslint-plugin-jest-dom" { - type Plugin = import("eslint").ESLint.Plugin; - type Rules = import("eslint").Linter.RulesRecord; - type LanguageOptions = import("eslint").Linter.Config["languageOptions"]; - - const plugin: Plugin; + import type { ESLint, Linter } from "eslint"; interface Configs { "flat/recommended": { plugins: string[]; - rules: Rules; + rules: Linter.RulesRecord; }; } export const configs: Configs; - - export default Plugin; + export default ESLint.Plugin; } declare module "eslint-plugin-react-refresh" { - type Plugin = import("eslint").ESLint.Plugin; + import type { ESLint } from "eslint"; + + export default ESLint.Plugin; +} + +// TODO: remove when https://github.com/eslint-community/eslint-plugin-eslint-comments/issues/214 is resolved. - const plugin: Plugin; +declare module "@eslint-community/eslint-plugin-eslint-comments/configs" { + import type { Linter } from "eslint"; + + declare namespace Configs { + import defaultExports = Configs; + + export const recommended: Linter.Config; + + export { defaultExports as default }; + } - export default plugin; + export = Configs; }