@hannoeru/eslint-plugin
Version:
My eslint plugin
805 lines (789 loc) • 25.3 kB
JavaScript
import config from 'eslint-config-standard/.eslintrc.json';
const TSEquivalents = [
"comma-spacing",
"brace-style",
"func-call-spacing",
"indent",
"keyword-spacing",
"lines-between-class-members",
"no-array-constructor",
"no-dupe-class-members",
"no-redeclare",
"no-unused-vars",
"no-unused-expressions",
"no-useless-constructor",
"no-use-before-define",
"quotes",
"semi",
"space-before-function-paren",
"space-infix-ops",
"object-curly-spacing"
];
const VueEquivalents = [
// Extension rules
"array-bracket-spacing",
"arrow-spacing",
"block-spacing",
"brace-style",
"camelcase",
"comma-dangle",
"comma-spacing",
"comma-style",
"dot-location",
"dot-notation",
"eqeqeq",
"func-call-spacing",
"key-spacing",
"keyword-spacing",
"no-constant-condition",
"no-empty-pattern",
"no-extra-parens",
"no-irregular-whitespace",
"no-loss-of-precision",
"no-sparse-arrays",
"object-curly-newline",
"object-curly-spacing",
"object-property-newline",
"object-shorthand",
"operator-linebreak",
"quote-props",
"space-in-parens",
"space-infix-ops",
"space-unary-ops",
"template-curly-spacing"
];
const configStandard = config;
const resolverExtensions = [".js", ".jsx", ".mjs", ".cjs", ".json"];
const resolverExtensionsWithTS = [".ts", ".tsx", ...resolverExtensions];
function defineConfig(config) {
return config;
}
function definePlugin(plugin) {
return plugin;
}
const ruleFromStandard = (name) => {
if (configStandard.rules === void 0)
throw new Error("rules can not be undefined");
const rule = configStandard.rules[name];
if (rule === void 0)
throw new Error("rule can not be undefined");
if (typeof rule !== "object")
return rule;
return JSON.parse(JSON.stringify(rule));
};
function fromEntries(iterable) {
return [...iterable].reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
}
const ignorePatterns = [
"*.min.*",
"CHANGELOG.md",
"dist",
"LICENSE*",
"output",
"coverage",
"public",
"temp",
"packages-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"__snapshots__",
"!.github",
"!.vitepress",
"!.vscode"
];
const core = defineConfig({
plugins: ["@hannoeru", "html", "eslint-comments"],
extends: ["standard"],
ignorePatterns,
rules: {
// Common
"curly": ["error", "multi-line"],
"quote-props": ["error", "consistent-as-needed"],
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-param-reassign": "off",
"camelcase": "off",
"comma-dangle": ["error", "always-multiline"],
"no-console": "error",
"no-cond-assign": ["error", "always"],
"no-return-await": "error",
"operator-linebreak": ["error", "before"],
"space-before-function-paren": ["error", "never"],
"no-use-before-define": ["error", {
functions: false,
classes: false,
variables: true
}],
"no-restricted-syntax": [
"error",
"DebuggerStatement",
"LabeledStatement",
"WithStatement"
],
// formatting
"spaced-comment": ["error", "always", {
line: {
markers: ["/"],
exceptions: ["/", "#"]
},
block: {
markers: ["!"],
exceptions: ["*"],
balanced: true
}
}],
"sort-imports": [
"error",
{
ignoreCase: false,
ignoreDeclarationSort: true,
ignoreMemberSort: false,
memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
allowSeparatedGroups: false
}
],
// eslint-comments
// Require a eslint-enable comment for every eslint-disable comment
"eslint-comments/disable-enable-pair": ["error", { allowWholeFile: true }],
// Disallow a eslint-enable comment for multiple eslint-disable comments
"eslint-comments/no-aggregating-enable": "error",
// Disallow duplicate eslint-disable comments
"eslint-comments/no-duplicate-disable": "error",
// Disallow eslint-disable comments without rule names
"eslint-comments/no-unlimited-disable": "error",
// Disallow unused eslint-disable comments
"eslint-comments/no-unused-disable": "error",
// Disallow unused eslint-enable comments
"eslint-comments/no-unused-enable": "error",
// Disallow eslint-disable comments about specific rules
"eslint-comments/no-restricted-disable": "off",
// Disallow ESLint directive-comments entirely
"eslint-comments/no-use": "off",
// best-practice
"block-scoped-var": "error",
"eqeqeq": ["error", "smart"],
"no-alert": "warn",
"vars-on-top": "error"
}
});
const esnext = defineConfig({
env: {
es6: true,
browser: true,
node: true,
es2021: true
},
parserOptions: {
ecmaVersion: 2021,
sourceType: "module"
},
extends: [
"plugin:@hannoeru/core",
"plugin:import/recommended",
"plugin:promise/recommended"
],
plugins: ["unicorn"],
settings: {
"import/resolver": {
node: {
extensions: resolverExtensions
}
}
},
overrides: [
{
files: ["scripts/**/*"],
rules: {
"no-console": "off"
}
},
{
files: ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"],
rules: {
"no-unused-expressions": "off"
}
}
],
rules: {
"no-var": "error",
"prefer-const": [
"error",
{
destructuring: "any",
ignoreReadBeforeAssign: true
}
],
"prefer-arrow-callback": [
"error",
{
allowNamedFunctions: false,
allowUnboundThis: true
}
],
"object-shorthand": [
"error",
"always",
{
ignoreConstructors: false,
avoidQuotes: true
}
],
"prefer-exponentiation-operator": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
"generator-star-spacing": ["error", { before: true, after: false }],
// import
"import/order": [
"error",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"type"
],
pathGroups: [
{
pattern: "@/**",
group: "external",
position: "after"
},
{
pattern: "~/**",
group: "external",
position: "after"
}
],
pathGroupsExcludedImportTypes: ["type"]
}
],
"import/first": "error",
"import/no-mutable-exports": "error",
"import/no-unresolved": "off",
"import/no-absolute-path": "off",
"import/no-named-as-default-member": "off",
// promise
"promise/always-return": "off",
"promise/catch-or-return": "off",
// unicorns
// Pass error message when throwing errors
"unicorn/error-message": "error",
// Uppercase regex escapes
"unicorn/escape-case": "error",
// Array.isArray instead of instanceof
"unicorn/no-instanceof-array": "error",
// Prevent deprecated `new Buffer()`
"unicorn/no-new-buffer": "error",
// Keep regex literals safe!
"unicorn/no-unsafe-regex": "off",
// Lowercase number formatting for octal, hex, binary (0x1'error' instead of 0X1'error')
"unicorn/number-literal-case": "error",
// includes over indexOf when checking for existence
"unicorn/prefer-includes": "error",
// String methods startsWith/endsWith instead of more complicated stuff
"unicorn/prefer-starts-ends-with": "error",
// textContent instead of innerText
"unicorn/prefer-text-content": "error",
// Enforce throwing type error when throwing error while checking typeof
"unicorn/prefer-type-error": "error",
// Use new when throwing error
"unicorn/throw-new-error": "error"
}
});
const react = defineConfig({
overrides: [
{
files: ["*.jsx", "*.tsx"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended"
],
settings: {
react: {
version: "17"
}
},
rules: {
"jsx-quotes": [
"error",
"prefer-double"
],
// off
"react/prop-types": "off",
"react/no-unknown-property": "off",
// Prevent usage of button elements without an explicit type attribute
"react/button-has-type": "off",
// Prevent missing displayName in a React component definition
"react/display-name": ["error", { ignoreTranspilerName: false }],
// Prevent extraneous defaultProps on components
"react/default-props-match-prop-types": "error",
// Forbid foreign propTypes; forbids using another component's prop types unless they are explicitly imported/exported
"react/forbid-foreign-prop-types": "error",
// Forbid certain propTypes
"react/forbid-prop-types": ["error", { forbid: ["any", "array"] }],
// Standardize the way function component get defined
"react/function-component-definition": [
"error",
{ namedComponents: "arrow-function", unnamedComponents: "arrow-function" }
],
// Enforce using <> instead of <React.Fragment> for fragments
"react/jsx-fragments": ["error", "syntax"],
// Prevent using this.state within a this.setState
"react/no-access-state-in-setstate": "error",
// Prevent using Array index in key prop
"react/no-array-index-key": "error",
// Prevent usage of setState in componentDidUpdate
"react/no-did-update-set-state": "error",
// Prevent usage of shouldComponentUpdate when extending React.PureComponent
"react/no-redundant-should-component-update": "error",
// Prevent this from being used in stateless functional components
"react/no-this-in-sfc": "error",
// Prevent common casing typos
"react/no-typos": "error",
// Prevent usage of UNSAFE_ methods
"react/no-unsafe": ["error", { checkAliases: true }],
// Prevent definitions of unused prop types
"react/no-unused-prop-types": "error",
// Attempts to discover all state fields in a React component and warn if any of them are never read.
"react/no-unused-state": "error",
// Prevent usage of setState in componentWillUpdate
"react/no-will-update-set-state": "error",
// Enforce ES5 or ES6 class for React Components
"react/prefer-es6-class": "error",
// Enforce stateless React Components to be written as a pure function
"react/prefer-stateless-function": ["error", { ignorePureComponents: true }],
// Prevent extra closing tags for components without children
"react/self-closing-comp": "error",
// Enforce state initialization style
"react/state-in-constructor": ["error", "never"],
// Enforce style prop value being an object
"react/style-prop-object": "error",
// Prevent void DOM elements (e.g. <img />, <br />) from receiving children
"react/void-dom-elements-no-children": "error",
// JSX
// Enforce a new line after jsx elements and expressions
"react/jsx-newline": "off",
// Prevent usage of javascript: in URLs
"react/jsx-no-script-url": "error",
// Prevent react contexts from taking non-stable values
"react/jsx-no-constructed-context-values": "error",
// Enforce boolean attributes notation in JSX
"react/jsx-boolean-value": "error",
// Enforce or disallow spaces inside of curly braces in JSX attributes and expressions
"react/jsx-child-element-spacing": "error",
// Validate closing bracket location in JSX
"react/jsx-closing-bracket-location": ["error", { location: "tag-aligned" }],
// Validate closing tag location in JSX
"react/jsx-closing-tag-location": "error",
// Enforce curly braces or disallow unnecessary curly braces in JSX props and/or children
"react/jsx-curly-brace-presence": ["error", { propElementValues: "always" }],
// Enforce or disallow spaces inside of curly braces in JSX attributes
"react/jsx-curly-spacing": ["error", "never", { allowMultiline: true }],
// Enforce or disallow spaces around equal signs in JSX attributes
"react/jsx-equals-spacing": ["error", "never"],
// Restrict file extensions that may contain JSX
"react/jsx-filename-extension": ["error", { extensions: [".jsx", ".tsx"] }],
// Validate props indentation in JSX
"react/jsx-indent-props": ["error", 2],
// Validate JSX indentation
"react/jsx-indent": ["error", 2],
"react/jsx-key": [
"error",
{
checkFragmentShorthand: true,
checkKeyMustBeforeSpread: true
}
],
// Enforce position of the first prop in JSX
"react/jsx-first-prop-new-line": ["error", "multiline"],
// Limit maximum of props on a single line in JSX
"react/jsx-max-props-per-line": ["error", { maximum: { single: 5, multi: 1 } }],
// Disallow unnecessary fragments
"react/jsx-no-useless-fragment": ["error", { allowExpressions: true }],
// Limits every line in JSX to one expression each
"react/jsx-one-expression-per-line": "off",
// Enforce PascalCase for user-defined JSX components
"react/jsx-pascal-case": [
"error",
{
allowNamespace: true,
allowLeadingUnderscore: true
}
],
// Disallow multiple spaces between inline JSX props (fixable)
"react/jsx-props-no-multi-spaces": "error",
// Validate whitespace in and around the JSX opening and closing brackets
"react/jsx-tag-spacing": "error",
// Prevent missing parentheses around multilines JSX
"react/jsx-wrap-multilines": "error"
}
}
]
});
const next = defineConfig({
extends: [
"plugin:@hannoeru/typescript",
"plugin:@hannoeru/react",
"plugin:@hannoeru/json",
"plugin:@hannoeru/yml",
"plugin:@next/next/recommended"
],
plugins: ["jsx-a11y"],
rules: {
"import/no-anonymous-default-export": "warn",
"jsx-a11y/alt-text": [
"warn",
{
elements: ["img"],
img: ["Image"]
}
],
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn"
}
});
const json = defineConfig({
extends: [
"plugin:jsonc/recommended-with-jsonc"
],
overrides: [
{
files: ["*.json", "*.json5", "*.jsonc"],
parser: "jsonc-eslint-parser",
rules: {
"jsonc/array-bracket-spacing": "error",
"jsonc/comma-style": ["error", "last"],
"jsonc/indent": ["error", 2],
"jsonc/key-spacing": ["error", { beforeColon: false, afterColon: true }],
"jsonc/no-octal-escape": "error",
"jsonc/object-curly-newline": ["error", { multiline: true, consistent: true }],
"jsonc/object-curly-spacing": ["error", "always"],
"jsonc/object-property-newline": ["error", { allowMultiplePropertiesPerLine: true }]
}
},
{
files: ["package.json"],
parser: "jsonc-eslint-parser",
rules: {
"jsonc/sort-keys": [
"error",
{
pathPattern: "^$",
order: [
"name",
"type",
"version",
"private",
"packageManager",
"description",
"keywords",
"license",
"author",
"repository",
"funding",
"main",
"module",
"types",
"unpkg",
"jsdelivr",
"exports",
"files",
"bin",
"sideEffects",
"scripts",
"peerDependencies",
"peerDependenciesMeta",
"dependencies",
"optionalDependencies",
"devDependencies",
"husky",
"lint-staged",
"eslintConfig"
]
},
{
pathPattern: "^(?:dev|peer|optional|bundled)?[Dd]ependencies$",
order: { type: "asc" }
}
]
}
}
]
});
const jest = defineConfig({
extends: [
"plugin:jest/style",
"plugin:jest/recommended",
"plugin:jest-formatting/recommended"
],
settings: {
jest: {
version: 26
}
},
rules: {
"jest/prefer-lowercase-title": ["error", { ignore: ["describe"] }],
"jest/no-restricted-matchers": [
"error",
{
toBeTruthy: "Avoid `toBeTruthy`",
toBeFalsy: "Avoid `toBeFalsy`",
resolves: "Use `expect(await promise)` instead."
}
],
"jest/valid-title": [
"error",
{
disallowedWords: [
"correct",
"appropriate",
"properly",
"should",
"every",
"descriptive"
]
}
]
}
});
const vue = defineConfig({
overrides: [
{
files: ["*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser"
},
extends: ["plugin:vue/vue3-recommended"],
rules: {
"no-undef": "off",
"no-unused-vars": "off",
// Override Recommended
"vue/max-attributes-per-line": ["warn", { singleline: 5 }],
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
// Vue versions of Standard.js rules:
...fromEntries(VueEquivalents.map((name) => [`vue/${name}`, ruleFromStandard(name)])),
// Override Standard.js rules
"vue/quote-props": ["error", "consistent-as-needed"],
"vue/no-unused-vars": ["error", { ignorePattern: "^_" }],
"vue/camelcase": "off",
"vue/comma-dangle": ["error", "always-multiline"],
// Uncategorized rules
"vue/block-tag-newline": ["error", {
singleline: "always",
multiline: "always"
}],
"vue/component-name-in-template-casing": ["error", "PascalCase"],
"vue/component-options-name-casing": ["error", "PascalCase"],
"vue/custom-event-name-casing": ["error", "camelCase"],
"vue/define-macros-order": ["error", {
order: ["defineProps", "defineEmits"]
}],
"vue/html-comment-content-spacing": ["error", "always"],
"vue/html-comment-indent": ["error", 2],
"vue/no-restricted-v-bind": ["error", "/^v-/"],
"vue/no-useless-v-bind": "error",
"vue/padding-line-between-blocks": ["error", "always"],
"vue/prefer-separate-static-class": "error"
}
}
]
});
const yml = defineConfig({
extends: [
"plugin:yml/standard"
],
rules: {
"yml/quotes": ["error", { prefer: "single", avoidEscape: false }],
"yml/no-empty-document": "off",
"yml/no-empty-mapping-value": "off"
}
});
const markdown = defineConfig({
extends: [
"plugin:markdown/recommended"
],
overrides: [
{
files: ["**/*.md"],
rules: {
indent: "off"
}
},
{
// Code blocks in markdown file
files: ["**/*.md/*.*"],
rules: {
"no-alert": "off",
"no-console": "off",
"no-restricted-imports": "off",
"no-undef": "off",
"no-unused-vars": "off",
"no-unused-expressions": "off",
"react/jsx-no-undef": "off",
"import/no-unresolved": "off",
"import/namespace": "off",
"import/default": "off",
"import/no-named-as-default": "off",
"@typescript-eslint/no-redeclare": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
]
});
const typescript = defineConfig({
extends: [
"plugin:@hannoeru/esnext",
"plugin:import/typescript"
],
settings: {
"import/extensions": resolverExtensionsWithTS,
"import/external-module-folders": ["node_modules", "node_modules/@types"],
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
node: {
extensions: resolverExtensionsWithTS
},
typescript: {
alwaysTryTypes: true
}
}
},
overrides: [
{
files: ["*.d.ts"],
rules: {
"import/no-duplicates": "off"
}
},
{
files: ["*.ts", "*.tsx"],
extends: ["plugin:@typescript-eslint/recommended"],
rules: {
// Rules replaced by @typescript-eslint versions:
...fromEntries(TSEquivalents.map((name) => [name, "off"])),
// @typescript-eslint versions of Standard.js rules:
...fromEntries(TSEquivalents.map((name) => [`@typescript-eslint/${name}`, ruleFromStandard(name)])),
// Override custom JS rules
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-use-before-define": ["error", {
functions: false,
classes: false,
enums: false,
variables: true,
typedefs: false
// Only the TypeScript rule has this option.
}],
"@typescript-eslint/comma-dangle": ["error", "always-multiline"],
"@typescript-eslint/space-before-function-paren": ["error", "never"],
// TS
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }],
"@typescript-eslint/member-delimiter-style": [
"error",
{
multiline: { delimiter: "none" },
singleline: { delimiter: "comma", requireLast: false }
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports", disallowTypeAnnotations: false }],
"@typescript-eslint/consistent-indexed-object-style": ["error", "record"],
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
"@typescript-eslint/prefer-ts-expect-error": "error",
// off
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/parameter-properties": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-namespace": "off"
}
}
]
});
const typescriptTypeChecking = defineConfig({
overrides: [
{
files: ["*.ts", "*.tsx"],
rules: {
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"no-implied-eval": "off",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
"require-await": "off",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/restrict-template-expressions": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/no-unnecessary-condition": ["error"],
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-readonly": [
"error",
{ onlyInlineLambdas: false }
],
"@typescript-eslint/promise-function-async": "error",
"dot-notation": "off",
"@typescript-eslint/dot-notation": "error",
"no-throw-literal": "off",
"@typescript-eslint/no-throw-literal": "error"
}
}
]
});
const plugin = definePlugin({
rules: {},
configs: {
// basic
core,
esnext,
// frameworks
vue,
react,
next,
// ts
typescript,
typescriptTypeChecking,
// config format
yml,
json,
// test
jest,
// docs
markdown
}
});
const { rules, configs } = plugin;
export { configs, rules };