2023-03-15 06:42:14 +03:00
|
|
|
/** @file ESLint configuration file. */
|
|
|
|
/** NOTE: The "Experimental: Use Flat Config" option must be enabled.
|
|
|
|
* Flat config is still not quite mature, so is disabled by default. */
|
|
|
|
import * as path from 'node:path'
|
|
|
|
import * as url from 'node:url'
|
|
|
|
|
2023-06-19 02:02:08 +03:00
|
|
|
// The preferred syntax is `import * as name`, however these modules do not support it.
|
2023-03-15 06:42:14 +03:00
|
|
|
// This is specialcased in other files, but these modules shouldn't be used in other files anyway.
|
|
|
|
/* eslint-disable no-restricted-syntax */
|
|
|
|
import eslintJs from '@eslint/js'
|
|
|
|
import globals from 'globals'
|
|
|
|
import jsdoc from 'eslint-plugin-jsdoc'
|
2023-07-06 14:52:32 +03:00
|
|
|
import react from 'eslint-plugin-react'
|
|
|
|
import reactHooks from 'eslint-plugin-react-hooks'
|
2023-03-15 06:42:14 +03:00
|
|
|
import tsEslint from '@typescript-eslint/eslint-plugin'
|
|
|
|
import tsEslintParser from '@typescript-eslint/parser'
|
|
|
|
/* eslint-enable no-restricted-syntax */
|
|
|
|
|
2023-05-19 22:55:29 +03:00
|
|
|
// =================
|
|
|
|
// === Constants ===
|
|
|
|
// =================
|
|
|
|
|
2023-03-15 06:42:14 +03:00
|
|
|
const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url))
|
|
|
|
const NAME = 'enso'
|
|
|
|
/** An explicit whitelist of CommonJS modules, which do not support namespace imports.
|
|
|
|
* Many of these have incorrect types, so no type error may not mean they support ESM,
|
|
|
|
* and conversely type errors may not mean they don't support ESM -
|
|
|
|
* but we add those to the whitelist anyway otherwise we get type errors.
|
2023-03-15 14:54:16 +03:00
|
|
|
* In particular, `string-length` supports ESM but its type definitions don't.
|
2023-07-26 15:59:48 +03:00
|
|
|
* `yargs` is a modules we explicitly want the default imports of.
|
2023-03-16 16:09:31 +03:00
|
|
|
* `node:process` is here because `process.on` does not exist on the namespace import. */
|
2023-03-15 14:54:16 +03:00
|
|
|
const DEFAULT_IMPORT_ONLY_MODULES =
|
2023-10-11 13:24:33 +03:00
|
|
|
'node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml'
|
2023-07-26 15:59:48 +03:00
|
|
|
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss`
|
|
|
|
const OUR_MODULES = 'enso-.*'
|
2023-03-15 06:42:14 +03:00
|
|
|
const RELATIVE_MODULES =
|
2023-08-09 12:30:40 +03:00
|
|
|
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations'
|
2023-03-20 12:35:16 +03:00
|
|
|
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
2023-03-15 06:42:14 +03:00
|
|
|
const JSX = ':matches(JSXElement, JSXFragment)'
|
2023-07-19 12:48:39 +03:00
|
|
|
const NOT_PASCAL_CASE = '/^(?!do[A-Z])(?!_?([A-Z][a-z0-9]*)+$)/'
|
2023-06-19 02:02:08 +03:00
|
|
|
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)(?!React$)/'
|
2023-07-18 14:23:41 +03:00
|
|
|
const WHITELISTED_CONSTANTS = 'logger|.+Context|interpolationFunction.+'
|
2023-03-15 06:42:14 +03:00
|
|
|
const NOT_CONSTANT_CASE = `/^(?!${WHITELISTED_CONSTANTS}$|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$)/`
|
2023-05-19 22:55:29 +03:00
|
|
|
|
|
|
|
// =======================================
|
|
|
|
// === Restricted syntactic constructs ===
|
|
|
|
// =======================================
|
2023-03-15 06:42:14 +03:00
|
|
|
|
|
|
|
// Extracted to a variable because it needs to be used twice:
|
|
|
|
// - once as-is for `.d.ts`
|
|
|
|
// - once explicitly disallowing `declare`s in regular `.ts`.
|
|
|
|
/** @type {{ selector: string; message: string; }[]} */
|
|
|
|
const RESTRICTED_SYNTAXES = [
|
|
|
|
{
|
|
|
|
selector:
|
|
|
|
':matches(ImportDeclaration:has(ImportSpecifier), ExportDeclaration, ExportSpecifier)',
|
|
|
|
message: 'No {} imports and exports',
|
|
|
|
},
|
|
|
|
{
|
2023-03-15 14:54:16 +03:00
|
|
|
selector: `ImportDeclaration[source.value=/^(?!(${ALLOWED_DEFAULT_IMPORT_MODULES})$)[^.]/] > ImportDefaultSpecifier`,
|
2023-03-15 06:42:14 +03:00
|
|
|
message:
|
2023-03-15 14:54:16 +03:00
|
|
|
'No default imports from modules. Add to `DEFAULT_IMPORT_ONLY_MODULES` in `eslint.config.js` if the module only has a default export.',
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
{
|
2023-03-15 14:54:16 +03:00
|
|
|
selector: `ImportDeclaration[source.value=/^(?:${DEFAULT_IMPORT_ONLY_MODULES})$/] > ImportNamespaceSpecifier`,
|
|
|
|
message: 'No namespace imports from modules that only have a default import',
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/\\.(?:json|yaml|yml)$/] > ImportDefaultSpecifier[local.name=${NOT_CONSTANT_CASE}]`,
|
|
|
|
message: 'Use `CONSTANT_CASE` for imports from data files',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/\\.json$/]:not(:has(ImportAttribute[key.name=type][value.value=json]))`,
|
|
|
|
message: "JSON imports must be { type: 'json' }",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/\\.(?:yaml|yml)$/]:not(:has(ImportAttribute[key.name=type][value.value=yaml]))`,
|
|
|
|
message: "YAML imports must be { type: 'yaml' }",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/\\.(?:json|yaml|yml)$/] > ImportNamespaceSpecifier`,
|
|
|
|
message: 'Use default imports for imports from data files',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportNamespaceSpecifier > Identifier[name=${NOT_CAMEL_CASE}]`,
|
|
|
|
message: 'Use `camelCase` for imports',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `:matches(ImportDefaultSpecifier[local.name=/^${NAME}/i], ImportNamespaceSpecifier > Identifier[name=/^${NAME}/i])`,
|
|
|
|
message: `Don't prefix modules with \`${NAME}\``,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'ExportAllDeclaration',
|
|
|
|
message: 'No re-exports',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'TSTypeLiteral',
|
|
|
|
message: 'No object types - use interfaces instead',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'ForOfStatement > .left[kind=let]',
|
|
|
|
message: 'Use `for (const x of xs)`, not `for (let x of xs)`',
|
|
|
|
},
|
|
|
|
{
|
2023-07-19 12:48:39 +03:00
|
|
|
selector:
|
|
|
|
'TSTypeAliasDeclaration > TSTypeReference:not(:has(.typeParameters)) > Identifier',
|
2023-03-15 06:42:14 +03:00
|
|
|
message: 'No renamed types',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'TSTypeAliasDeclaration > :matches(TSLiteralType)',
|
|
|
|
message: 'No aliases to literal types',
|
|
|
|
},
|
2023-03-18 03:22:04 +03:00
|
|
|
{
|
|
|
|
selector:
|
2023-04-24 15:56:26 +03:00
|
|
|
':not(:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, SwitchStatement, SwitchCase, IfStatement:has(.consequent > :matches(ReturnStatement, ThrowStatement)):has(.alternate :matches(ReturnStatement, ThrowStatement)), Program > TryStatement, Program > TryStatement > .handler, TryStatement:has(.block > :matches(ReturnStatement, ThrowStatement)):has(:matches([handler=null], .handler :matches(ReturnStatement, ThrowStatement))), TryStatement:has(.block > :matches(ReturnStatement, ThrowStatement)):has(:matches([handler=null], .handler :matches(ReturnStatement, ThrowStatement))) > .handler)) > * > :matches(ReturnStatement, ThrowStatement)',
|
2023-03-18 03:22:04 +03:00
|
|
|
message: 'No early returns',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
{
|
|
|
|
selector:
|
|
|
|
'TSTypeAliasDeclaration > :matches(TSBooleanKeyword, TSBigintKeyword, TSNullKeyword, TSNumberKeyword, TSObjectKeyword, TSStringKeyword, TSSymbolKeyword, TSUndefinedKeyword, TSUnknownKeyword, TSVoidKeyword)',
|
|
|
|
message:
|
|
|
|
'No aliases to primitives - consider using brands instead: `string & { _brand: "BrandName"; }`',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// Matches functions and arrow functions, but not methods.
|
2023-08-31 14:30:01 +03:00
|
|
|
selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(${JSX}), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression.init ${JSX})))`,
|
2023-03-15 06:42:14 +03:00
|
|
|
message: 'Use `PascalCase` for React components',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// Matches other functions, non-consts, and consts not at the top level.
|
|
|
|
selector: `:matches(FunctionDeclaration[id.name=${NOT_CAMEL_CASE}]:not(:has(${JSX})), VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:has(ArrowFunctionExpression.init:not(:has(${JSX}))), :matches(VariableDeclaration[kind^=const], Program :not(ExportNamedDeclaration, TSModuleBlock) > VariableDeclaration[kind=const], ExportNamedDeclaration > * VariableDeclaration[kind=const]) > VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:not(:has(ArrowFunctionExpression)))`,
|
|
|
|
message: 'Use `camelCase` for everything but React components',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// Matches non-functions.
|
2023-07-19 12:48:39 +03:00
|
|
|
selector: `:matches(Program, ExportNamedDeclaration, TSModuleBlock) > VariableDeclaration[kind=const] > VariableDeclarator[id.name=${NOT_CONSTANT_CASE}]:not(:matches(:has(ArrowFunctionExpression), :has(CallExpression[callee.object.name=newtype][callee.property.name=newtypeConstructor])))`,
|
2023-03-15 06:42:14 +03:00
|
|
|
message: 'Use `CONSTANT_CASE` for top-level constants that are not functions',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `:matches(Program, ExportNamedDeclaration, TSModuleBlock) > VariableDeclaration > VariableDeclarator > ArrowFunctionExpression`,
|
|
|
|
message: 'Use `function foo() {}` instead of `const foo = () => {}`',
|
|
|
|
},
|
2023-03-17 18:26:59 +03:00
|
|
|
{
|
|
|
|
selector: `ClassBody > PropertyDefinition > ArrowFunctionExpression`,
|
|
|
|
message: 'Use `foo() {}` instead of `foo = () => {}`',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
{
|
|
|
|
selector: `:matches(Program, ExportNamedDeclaration) > VariableDeclaration[kind=const] > * > ObjectExpression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL})))`,
|
|
|
|
message: 'Use `as const` for top-level object literals only containing string literals',
|
|
|
|
},
|
|
|
|
{
|
2023-03-20 12:35:16 +03:00
|
|
|
// Matches `as T` in either:
|
|
|
|
// - anything other than a variable declaration
|
|
|
|
// - a variable declaration that is not at the top level
|
|
|
|
// - a top-level variable declaration that shouldn't be `as const`
|
|
|
|
// - a top-level variable declaration that should be `as const`, but is `as SomeActualType` instead
|
|
|
|
selector: `:matches(:not(VariableDeclarator) > TSAsExpression, :not(:matches(Program, ExportNamedDeclaration)) > VariableDeclaration > * > TSAsExpression, :matches(Program, ExportNamedDeclaration) > VariableDeclaration > * > TSAsExpression > .expression:not(ObjectExpression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL})))), :matches(Program, ExportNamedDeclaration) > VariableDeclaration > * > TsAsExpression:not(:has(TSTypeReference > Identifier[name=const])) > ObjectExpression.expression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL}))))`,
|
2023-07-26 15:59:48 +03:00
|
|
|
// This cannot be changed right now, as `cognito.ts` would need to be refactored.
|
|
|
|
// selector: `:matches(:not(VariableDeclarator) > TSAsExpression, VariableDeclaration > * > TSAsExpression)`,
|
2023-03-20 12:35:16 +03:00
|
|
|
message: 'Avoid `as T`. Consider using a type annotation instead.',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector:
|
|
|
|
':matches(TSUndefinedKeyword, Identifier[name=undefined], UnaryExpression[operator=void]:not(:has(CallExpression.argument)), BinaryExpression[operator=/^===?$/]:has(UnaryExpression.left[operator=typeof]):has(Literal.right[value=undefined]))',
|
|
|
|
message: 'Use `null` instead of `undefined`, `void 0`, or `typeof x === "undefined"`',
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'ExportNamedDeclaration > VariableDeclaration[kind=let]',
|
|
|
|
message: 'Use `export const` instead of `export let`',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `Program > VariableDeclaration[kind=let] > * > ObjectExpression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL})))`,
|
|
|
|
message:
|
|
|
|
'Use `const` instead of `let` for top-level object literals only containing string literals',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector:
|
|
|
|
'ImportDeclaration[source.value=/^(?:assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|diagnostics_channel|dns|domain|events|fs|fs\\u002Fpromises|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|timers|tls|trace_events|tty|url|util|v8|vm|wasi|worker_threads|zlib)$/]',
|
|
|
|
message: 'Use `node:` prefix to import builtin node modules',
|
|
|
|
},
|
2023-04-06 13:00:55 +03:00
|
|
|
{
|
|
|
|
selector: 'TSEnumDeclaration:not(:has(TSEnumMember))',
|
|
|
|
message: 'Enums must not be empty',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
{
|
|
|
|
selector:
|
|
|
|
'ImportDeclaration[source.value=/^(?!node:)/] ~ ImportDeclaration[source.value=/^node:/]',
|
|
|
|
message:
|
|
|
|
'Import node modules before npm modules, our modules, and relative imports, separated by a blank line',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/^(?:${OUR_MODULES}|${RELATIVE_MODULES})$/] ~ ImportDeclaration[source.value=/^(?!(|${OUR_MODULES}|${RELATIVE_MODULES})$|\\.)/]`,
|
|
|
|
message:
|
|
|
|
'Import npm modules before our modules and relative imports, separated by a blank line',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/^(?:${RELATIVE_MODULES})$/] ~ ImportDeclaration[source.value=/^(?:${OUR_MODULES})$/]`,
|
|
|
|
message: 'Import our modules before relative imports, separated by a blank line',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/^\\./] ~ ImportDeclaration[source.value=/^[^.]/]`,
|
|
|
|
message: 'Import relative imports last',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: `ImportDeclaration[source.value=/^\\..+\\.(?:json|yml|yaml)$/] ~ ImportDeclaration[source.value=/^\\..+\\.(?!json|yml|yaml)[^.]+$/]`,
|
|
|
|
message: 'Import data files after other relative imports',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector:
|
|
|
|
'TSAsExpression:has(TSUnknownKeyword, TSNeverKeyword, TSAnyKeyword) > TSAsExpression',
|
|
|
|
message: 'Use type assertions to specific types instead of `unknown`, `any` or `never`',
|
|
|
|
},
|
2023-05-19 22:55:29 +03:00
|
|
|
{
|
|
|
|
selector: ':matches(MethodDeclaration, FunctionDeclaration) FunctionDeclaration',
|
|
|
|
message: 'Use arrow functions for nested functions',
|
|
|
|
},
|
|
|
|
{
|
2023-06-19 02:02:08 +03:00
|
|
|
selector:
|
|
|
|
':not(ExportNamedDeclaration) > TSInterfaceDeclaration[id.name=/^(?!Internal).+Props$/]',
|
2023-05-19 22:55:29 +03:00
|
|
|
message: 'All React component `Props` types must be exported',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: 'FunctionDeclaration:has(:matches(ObjectPattern.params, ArrayPattern.params))',
|
2023-07-26 15:59:48 +03:00
|
|
|
message: 'Destructure function parameters in the body, instead of in the parameter list',
|
2023-05-19 22:55:29 +03:00
|
|
|
},
|
2023-05-02 20:48:07 +03:00
|
|
|
{
|
|
|
|
selector: 'IfStatement > ExpressionStatement',
|
|
|
|
message: 'Wrap `if` branches in `{}`',
|
|
|
|
},
|
2023-07-19 12:48:39 +03:00
|
|
|
{
|
|
|
|
selector: ':matches(ForStatement[test=null], ForStatement[test.value=true])',
|
|
|
|
message: 'Use `while (true)` instead of `for (;;)`',
|
|
|
|
},
|
2023-08-09 12:30:40 +03:00
|
|
|
{
|
|
|
|
selector: 'CallExpression[callee.name=toastAndLog][arguments.0.value=/\\.$/]',
|
|
|
|
message: '`toastAndLog` already includes a trailing `.`',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
]
|
|
|
|
|
2023-05-19 22:55:29 +03:00
|
|
|
// ============================
|
|
|
|
// === ESLint configuration ===
|
|
|
|
// ============================
|
|
|
|
|
2023-03-15 06:42:14 +03:00
|
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
|
|
export default [
|
|
|
|
eslintJs.configs.recommended,
|
2023-10-11 13:24:33 +03:00
|
|
|
{
|
|
|
|
// Playwright build cache.
|
|
|
|
ignores: ['**/.cache/**'],
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
{
|
2023-06-19 02:02:08 +03:00
|
|
|
settings: {
|
|
|
|
react: {
|
|
|
|
version: '18.2',
|
|
|
|
},
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
plugins: {
|
|
|
|
jsdoc: jsdoc,
|
|
|
|
'@typescript-eslint': tsEslint,
|
2023-07-06 14:52:32 +03:00
|
|
|
react: react,
|
|
|
|
'react-hooks': reactHooks,
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
languageOptions: {
|
|
|
|
parser: tsEslintParser,
|
|
|
|
parserOptions: {
|
|
|
|
tsconfigRootDir: DIR_NAME,
|
|
|
|
project: true,
|
|
|
|
},
|
|
|
|
globals: {
|
|
|
|
...globals.browser,
|
|
|
|
...globals.node,
|
|
|
|
...globals.es2015,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
rules: {
|
|
|
|
...tsEslint.configs['eslint-recommended']?.rules,
|
|
|
|
...tsEslint.configs.recommended?.rules,
|
|
|
|
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
|
|
|
|
...tsEslint.configs.strict?.rules,
|
2023-07-06 14:52:32 +03:00
|
|
|
...react.configs.recommended.rules,
|
2023-03-20 12:35:16 +03:00
|
|
|
eqeqeq: ['error', 'always', { null: 'never' }],
|
2023-05-19 22:55:29 +03:00
|
|
|
'jsdoc/require-jsdoc': [
|
|
|
|
'error',
|
|
|
|
{
|
|
|
|
require: {
|
|
|
|
FunctionDeclaration: true,
|
|
|
|
MethodDefinition: true,
|
|
|
|
ClassDeclaration: true,
|
|
|
|
ArrowFunctionExpression: false,
|
|
|
|
FunctionExpression: true,
|
|
|
|
},
|
|
|
|
// Top-level constants should require JSDoc as well,
|
|
|
|
// however it does not seem like there is a way to do this.
|
|
|
|
contexts: [
|
|
|
|
'TSInterfaceDeclaration',
|
|
|
|
'TSEnumDeclaration',
|
|
|
|
'TSTypeAliasDeclaration',
|
|
|
|
'TSMethodSignature',
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
2023-03-15 06:42:14 +03:00
|
|
|
'sort-imports': ['error', { allowSeparatedGroups: true }],
|
2023-07-19 12:48:39 +03:00
|
|
|
'no-constant-condition': ['error', { checkLoops: false }],
|
2023-06-02 12:05:37 +03:00
|
|
|
'no-restricted-properties': [
|
|
|
|
'error',
|
|
|
|
{
|
|
|
|
object: 'router',
|
|
|
|
property: 'useNavigate',
|
|
|
|
message: 'Use `hooks.useNavigate` instead.',
|
|
|
|
},
|
|
|
|
],
|
2023-03-15 06:42:14 +03:00
|
|
|
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
|
2023-03-17 18:26:59 +03:00
|
|
|
'prefer-arrow-callback': 'error',
|
2023-06-19 02:02:08 +03:00
|
|
|
'prefer-const': 'error',
|
2023-07-06 14:52:32 +03:00
|
|
|
// Not relevant because TypeScript checks types.
|
|
|
|
'react/prop-types': 'off',
|
|
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
|
|
'react-hooks/exhaustive-deps': 'error',
|
2023-03-15 06:42:14 +03:00
|
|
|
// Prefer `interface` over `type`.
|
|
|
|
'@typescript-eslint/consistent-type-definitions': 'error',
|
2023-10-11 13:24:33 +03:00
|
|
|
'@typescript-eslint/consistent-type-imports': 'error',
|
2023-03-15 06:42:14 +03:00
|
|
|
'@typescript-eslint/member-ordering': 'error',
|
|
|
|
// Method syntax is not type-safe.
|
|
|
|
// See: https://typescript-eslint.io/rules/method-signature-style
|
|
|
|
'@typescript-eslint/method-signature-style': 'error',
|
|
|
|
'@typescript-eslint/naming-convention': [
|
|
|
|
'error',
|
|
|
|
{
|
|
|
|
selector: ['function'],
|
|
|
|
// PascalCase for React components, camelCase for all other situations
|
|
|
|
format: ['camelCase', 'PascalCase'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: ['variable'],
|
|
|
|
modifiers: ['const', 'global'],
|
|
|
|
format: ['UPPER_CASE', 'camelCase', 'PascalCase'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: ['variable'],
|
|
|
|
modifiers: ['const', 'exported'],
|
|
|
|
format: ['UPPER_CASE', 'camelCase', 'PascalCase'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: ['variable'],
|
|
|
|
format: ['camelCase', 'PascalCase'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: ['parameter', 'property', 'method'],
|
|
|
|
format: ['camelCase'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
selector: ['parameter'],
|
|
|
|
modifiers: ['unused'],
|
|
|
|
format: ['camelCase'],
|
|
|
|
leadingUnderscore: 'require',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
'@typescript-eslint/no-confusing-void-expression': 'error',
|
2023-04-26 12:52:13 +03:00
|
|
|
'@typescript-eslint/no-empty-interface': 'off',
|
2023-03-15 06:42:14 +03:00
|
|
|
'@typescript-eslint/no-extraneous-class': 'error',
|
2023-03-15 14:54:16 +03:00
|
|
|
'@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }],
|
2023-03-15 06:42:14 +03:00
|
|
|
// React 17 and later supports async functions as event handlers, so we need to disable this
|
|
|
|
// rule to avoid false positives.
|
|
|
|
//
|
|
|
|
// See: https://github.com/typescript-eslint/typescript-eslint/pull/4623
|
|
|
|
'@typescript-eslint/no-misused-promises': [
|
|
|
|
'error',
|
|
|
|
{ checksVoidReturn: { attributes: false } },
|
|
|
|
],
|
|
|
|
'@typescript-eslint/no-redundant-type-constituents': 'error',
|
2023-07-19 12:48:39 +03:00
|
|
|
'@typescript-eslint/no-unnecessary-condition': [
|
|
|
|
'error',
|
|
|
|
{ allowConstantLoopConditions: true },
|
|
|
|
],
|
2023-03-15 06:42:14 +03:00
|
|
|
'@typescript-eslint/no-useless-empty-export': 'error',
|
|
|
|
'@typescript-eslint/parameter-properties': ['error', { prefer: 'parameter-property' }],
|
|
|
|
'@typescript-eslint/prefer-enum-initializers': 'error',
|
|
|
|
'@typescript-eslint/prefer-readonly': 'error',
|
|
|
|
'@typescript-eslint/require-array-sort-compare': [
|
|
|
|
'error',
|
|
|
|
{ ignoreStringArrays: true },
|
|
|
|
],
|
|
|
|
'@typescript-eslint/restrict-template-expressions': 'error',
|
|
|
|
'@typescript-eslint/sort-type-constituents': 'error',
|
2023-07-06 14:52:32 +03:00
|
|
|
'@typescript-eslint/strict-boolean-expressions': 'error',
|
2023-03-15 06:42:14 +03:00
|
|
|
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
|
|
'default-param-last': 'off',
|
|
|
|
'@typescript-eslint/default-param-last': 'error',
|
|
|
|
'no-invalid-this': 'off',
|
|
|
|
'@typescript-eslint/no-invalid-this': ['error', { capIsConstructor: false }],
|
|
|
|
'jsdoc/no-magic-numbers': 'off',
|
|
|
|
'@typescript-eslint/no-magic-numbers': [
|
|
|
|
'error',
|
|
|
|
{
|
2023-10-11 13:24:33 +03:00
|
|
|
ignore: [-1, 0, 1, 2, 3, 4, 5],
|
2023-03-15 06:42:14 +03:00
|
|
|
ignoreArrayIndexes: true,
|
|
|
|
ignoreEnums: true,
|
|
|
|
detectObjects: true,
|
|
|
|
enforceConst: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
'no-redeclare': 'off',
|
|
|
|
// Important to warn on accidental duplicated `interface`s e.g. when writing API wrappers.
|
2023-04-06 13:00:55 +03:00
|
|
|
'@typescript-eslint/no-redeclare': ['error', { ignoreDeclarationMerge: false }],
|
2023-03-15 06:42:14 +03:00
|
|
|
'no-shadow': 'off',
|
2023-10-11 13:24:33 +03:00
|
|
|
'@typescript-eslint/no-shadow': 'error',
|
2023-03-15 06:42:14 +03:00
|
|
|
'no-unused-expressions': 'off',
|
|
|
|
'@typescript-eslint/no-unused-expressions': 'error',
|
|
|
|
'jsdoc/require-param-type': 'off',
|
2023-10-11 13:24:33 +03:00
|
|
|
'jsdoc/check-access': 'error',
|
|
|
|
'jsdoc/check-alignment': 'error',
|
|
|
|
'jsdoc/check-indentation': 'error',
|
|
|
|
'jsdoc/check-line-alignment': 'error',
|
|
|
|
'jsdoc/check-param-names': 'error',
|
|
|
|
'jsdoc/check-property-names': 'error',
|
|
|
|
'jsdoc/check-syntax': 'error',
|
|
|
|
'jsdoc/check-tag-names': 'error',
|
|
|
|
'jsdoc/check-types': 'error',
|
|
|
|
'jsdoc/check-values': 'error',
|
|
|
|
'jsdoc/empty-tags': 'error',
|
|
|
|
'jsdoc/implements-on-classes': 'error',
|
|
|
|
'jsdoc/no-bad-blocks': 'error',
|
|
|
|
'jsdoc/no-defaults': 'error',
|
|
|
|
'jsdoc/no-multi-asterisks': 'error',
|
|
|
|
'jsdoc/no-types': 'error',
|
|
|
|
'jsdoc/no-undefined-types': 'error',
|
|
|
|
'jsdoc/require-asterisk-prefix': 'error',
|
|
|
|
'jsdoc/require-description': 'error',
|
2023-03-17 18:26:59 +03:00
|
|
|
// This rule does not handle `# Heading`s and "etc.", "e.g.", "vs." etc.
|
2023-10-11 13:24:33 +03:00
|
|
|
// 'jsdoc/require-description-complete-sentence': 'error',
|
|
|
|
'jsdoc/require-file-overview': 'error',
|
|
|
|
'jsdoc/require-hyphen-before-param-description': 'error',
|
|
|
|
'jsdoc/require-param-description': 'error',
|
|
|
|
'jsdoc/require-param-name': 'error',
|
|
|
|
'jsdoc/require-property': 'error',
|
|
|
|
'jsdoc/require-property-description': 'error',
|
|
|
|
'jsdoc/require-property-name': 'error',
|
|
|
|
'jsdoc/require-property-type': 'error',
|
|
|
|
'jsdoc/require-returns-check': 'error',
|
|
|
|
'jsdoc/require-returns-description': 'error',
|
|
|
|
'jsdoc/require-throws': 'error',
|
|
|
|
'jsdoc/require-yields': 'error',
|
|
|
|
'jsdoc/require-yields-check': 'error',
|
|
|
|
'jsdoc/tag-lines': 'error',
|
|
|
|
'jsdoc/valid-types': 'error',
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
|
|
|
|
rules: {
|
|
|
|
'@typescript-eslint/no-var-requires': 'off',
|
|
|
|
// Parameter types must be specified using JSDoc in JS files.
|
|
|
|
'jsdoc/no-types': 'off',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.tsx', '**/*.mtsx', '**/*.ctsx'],
|
|
|
|
ignores: ['**/*.d.ts'],
|
|
|
|
rules: {
|
|
|
|
'no-restricted-syntax': [
|
|
|
|
'error',
|
|
|
|
...RESTRICTED_SYNTAXES,
|
|
|
|
{
|
|
|
|
selector: '[declare=true]',
|
|
|
|
message: 'No ambient declarations',
|
|
|
|
},
|
2023-07-26 15:59:48 +03:00
|
|
|
{
|
|
|
|
selector: 'ExportDefaultDeclaration:has(Identifier.declaration)',
|
|
|
|
message:
|
|
|
|
'Use `export default` on the declaration, instead of as a separate statement',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
],
|
|
|
|
// This rule does not work with TypeScript, and TypeScript already does this.
|
|
|
|
'no-undef': 'off',
|
|
|
|
},
|
|
|
|
},
|
2023-06-06 15:00:07 +03:00
|
|
|
{
|
|
|
|
files: [
|
|
|
|
'lib/dashboard/src/**/*.ts',
|
|
|
|
'lib/dashboard/src/**/*.mts',
|
|
|
|
'lib/dashboard/src/**/*.cts',
|
|
|
|
'lib/dashboard/src/**/*.tsx',
|
|
|
|
'lib/dashboard/src/**/*.mtsx',
|
|
|
|
'lib/dashboard/src/**/*.ctsx',
|
2023-10-11 13:24:33 +03:00
|
|
|
'lib/dashboard/mock/**/*.ts',
|
|
|
|
'lib/dashboard/mock/**/*.mts',
|
|
|
|
'lib/dashboard/mock/**/*.cts',
|
|
|
|
'lib/dashboard/mock/**/*.tsx',
|
|
|
|
'lib/dashboard/mock/**/*.mtsx',
|
|
|
|
'lib/dashboard/mock/**/*.ctsx',
|
2023-06-06 15:00:07 +03:00
|
|
|
],
|
|
|
|
rules: {
|
|
|
|
'no-restricted-properties': [
|
|
|
|
'error',
|
|
|
|
{
|
|
|
|
object: 'console',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugState',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
2023-06-19 02:02:08 +03:00
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugEffect',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugMemo',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugCallback',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
2023-08-10 12:09:31 +03:00
|
|
|
{
|
|
|
|
property: '$d$',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
2023-06-06 15:00:07 +03:00
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
2023-10-11 13:24:33 +03:00
|
|
|
{
|
|
|
|
files: [
|
|
|
|
'lib/dashboard/test*/**/*.ts',
|
|
|
|
'lib/dashboard/test*/**/*.mts',
|
|
|
|
'lib/dashboard/test*/**/*.cts',
|
|
|
|
'lib/dashboard/test*/**/*.tsx',
|
|
|
|
'lib/dashboard/test*/**/*.mtsx',
|
|
|
|
'lib/dashboard/test*/**/*.ctsx',
|
|
|
|
],
|
|
|
|
rules: {
|
|
|
|
'no-restricted-properties': [
|
|
|
|
'error',
|
|
|
|
{
|
|
|
|
object: 'console',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugState',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugEffect',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugMemo',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'hooks',
|
|
|
|
property: 'useDebugCallback',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
property: '$d$',
|
|
|
|
message: 'Avoid leaving debugging statements when committing code',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'page',
|
|
|
|
property: 'type',
|
|
|
|
message: 'Prefer `locator.type` instead',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'page',
|
|
|
|
property: 'click',
|
|
|
|
message: 'Prefer `locator.click` instead',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'page',
|
|
|
|
property: 'fill',
|
|
|
|
message: 'Prefer `locator.fill` instead',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
object: 'page',
|
|
|
|
property: 'locator',
|
|
|
|
message: 'Prefer `page.getBy*` instead',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
{
|
|
|
|
files: ['**/*.d.ts'],
|
2023-03-17 18:26:59 +03:00
|
|
|
rules: {
|
|
|
|
'no-undef': 'off',
|
|
|
|
},
|
2023-03-15 06:42:14 +03:00
|
|
|
},
|
|
|
|
]
|