Skip to content
Snippets Groups Projects
.eslintrc.js 19.5 KiB
Newer Older
const path = require('path');

const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = path.join(
  __dirname,
  'packages',
  'eslint-plugin-actual',
  'lib',
  'rules',
);

const ruleFCMsg =
  'Type the props argument and let TS infer or use ComponentType for a component prop';

const restrictedImportPatterns = [
  {
    group: ['*.api', '*.web', '*.electron'],
    message: 'Don’t directly reference imports from other platforms',
  },
  {
    group: ['uuid'],
    importNames: ['*'],
    message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
  },
const restrictedImportColors = [
  {
    group: ['**/style', '**/colors'],
    importNames: ['colors'],
    message: 'Please use themes instead of colors',
  },
];

module.exports = {
  root: true,
  env: {
    browser: true,
    commonjs: true,
    es6: true,
    jest: true,
    node: true,
  },
  plugins: [
    'prettier',
    'import',
    'rulesdir',
    '@typescript-eslint',
    'jsx-a11y',
    'react-hooks',
  ],
  extends: [
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/typescript',
  parser: '@typescript-eslint/parser',
  parserOptions: { project: [path.join(__dirname, './tsconfig.json')] },
  reportUnusedDisableDirectives: true,
  globals: {
    globalThis: false,
    // http://eslint.org/docs/rules/
    'array-callback-return': 'warn',
    'default-case': ['warn', { commentPattern: '^no default$' }],
    'dot-location': ['warn', 'property'],
    eqeqeq: ['warn', 'smart'],
    'new-parens': 'warn',
    'no-array-constructor': 'warn',
    'no-caller': 'warn',
    'no-cond-assign': ['warn', 'except-parens'],
    'no-const-assign': 'warn',
    'no-control-regex': 'warn',
    'no-delete-var': 'warn',
    'no-dupe-args': 'warn',
    'no-dupe-class-members': 'warn',
    'no-dupe-keys': 'warn',
    'no-duplicate-case': 'warn',
    'no-empty-character-class': 'warn',
    'no-empty-pattern': 'warn',
    'no-eval': 'warn',
    'no-ex-assign': 'warn',
    'no-extend-native': 'warn',
    'no-extra-bind': 'warn',
    'no-extra-label': 'warn',
    'no-fallthrough': 'warn',
    'no-func-assign': 'warn',
    'no-implied-eval': 'warn',
    'no-invalid-regexp': 'warn',
    'no-iterator': 'warn',
    'no-label-var': 'warn',
    'no-labels': ['warn', { allowLoop: true, allowSwitch: false }],
    'no-lone-blocks': 'warn',
    'no-mixed-operators': [
      'warn',
      {
        groups: [
          ['&', '|', '^', '~', '<<', '>>', '>>>'],
          ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
          ['&&', '||'],
          ['in', 'instanceof'],
        ],
        allowSamePrecedence: false,
      },
    ],
    'no-multi-str': 'warn',
    'no-global-assign': 'warn',
    'no-unsafe-negation': 'warn',
    'no-new-func': 'warn',
    'no-new-object': 'warn',
    'no-new-symbol': 'warn',
    'no-new-wrappers': 'warn',
    'no-obj-calls': 'warn',
    'no-octal': 'warn',
    'no-octal-escape': 'warn',
    'no-redeclare': 'warn',
    'no-regex-spaces': 'warn',
    'no-script-url': 'warn',
    'no-self-assign': 'warn',
    'no-self-compare': 'warn',
    'no-sequences': 'warn',
    'no-shadow-restricted-names': 'warn',
    'no-sparse-arrays': 'warn',
    'no-template-curly-in-string': 'warn',
    'no-this-before-super': 'warn',
    'no-throw-literal': 'warn',
    'no-undef': 'error',
    'no-unreachable': 'warn',
    'no-unused-expressions': [
      'error',
      {
        allowShortCircuit: true,
        allowTernary: true,
        allowTaggedTemplates: true,
      },
    ],
    'no-unused-labels': 'warn',
    'no-use-before-define': [
      'warn',
      {
        functions: false,
        classes: false,
        variables: false,
      },
    ],
    'no-useless-computed-key': 'warn',
    'no-useless-concat': 'warn',
    'no-useless-constructor': 'warn',
    'no-useless-escape': 'warn',
    'no-useless-rename': [
      'warn',
      {
        ignoreDestructuring: false,
        ignoreImport: false,
        ignoreExport: false,
      },
    ],
    'no-with': 'warn',
    'no-whitespace-before-property': 'warn',
    'react-hooks/exhaustive-deps': 'warn',
    'require-yield': 'warn',
    'rest-spread-spacing': ['warn', 'never'],
    strict: ['warn', 'never'],
    'unicode-bom': ['warn', 'never'],
    'use-isnan': 'warn',
    'valid-typeof': 'warn',
    'no-restricted-properties': [
      'error',
      {
        object: 'require',
        property: 'ensure',
        message:
          'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
      },
      {
        object: 'System',
        property: 'import',
        message:
          'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
      },
    ],
    'getter-return': 'warn',

    // https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
    'import/first': 'error',
    'import/no-amd': 'error',
    'import/no-anonymous-default-export': 'warn',
    'import/no-webpack-loader-syntax': 'error',

    // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
    'react/forbid-foreign-prop-types': ['warn', { allowInPropTypes: true }],
    'react/jsx-no-comment-textnodes': 'warn',
    'react/jsx-no-duplicate-props': 'warn',
    'react/jsx-no-target-blank': 'warn',
    'react/jsx-no-undef': 'error',
    'react/jsx-pascal-case': [
      'warn',
      {
        allowAllCaps: true,
        ignore: [],
      },
    ],
    'react/no-danger-with-children': 'warn',
    // Disabled because of undesirable warnings
    // See https://github.com/facebook/create-react-app/issues/5204 for
    // blockers until its re-enabled
    // 'react/no-deprecated': 'warn',
    'react/no-direct-mutation-state': 'warn',
    'react/no-is-mounted': 'warn',
    'react/no-typos': 'error',
    'react/require-render-return': 'error',
    'react/style-prop-object': 'warn',

    // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
    'jsx-a11y/alt-text': 'warn',
    'jsx-a11y/anchor-has-content': 'warn',
    'jsx-a11y/anchor-is-valid': [
      'warn',
      {
        aspects: ['noHref', 'invalidHref'],
      },
    ],
    'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
    'jsx-a11y/aria-props': 'warn',
    'jsx-a11y/aria-proptypes': 'warn',
    'jsx-a11y/aria-role': ['warn', { ignoreNonDOM: true }],
    'jsx-a11y/aria-unsupported-elements': 'warn',
    'jsx-a11y/heading-has-content': 'warn',
    'jsx-a11y/iframe-has-title': 'warn',
    'jsx-a11y/img-redundant-alt': 'warn',
    'jsx-a11y/no-access-key': 'warn',
    'jsx-a11y/no-distracting-elements': 'warn',
    'jsx-a11y/no-redundant-roles': 'warn',
    'jsx-a11y/role-has-required-aria-props': 'warn',
    'jsx-a11y/role-supports-aria-props': 'warn',
    'jsx-a11y/scope': 'warn',

    // https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
    'react-hooks/rules-of-hooks': 'error',


    // Note: base rule explicitly disabled in favor of the TS one
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': [
        ignoreRestSiblings: true,
    curly: ['warn', 'multi-line', 'consistent'],
    'no-restricted-globals': ['warn'].concat(
      require('confusing-browser-globals').filter(g => g !== 'self'),
    ),

    'react/jsx-filename-extension': [
      'warn',
      { extensions: ['.jsx', '.tsx'], allow: 'as-needed' },
    ],
    'react/jsx-no-useless-fragment': 'warn',
    'react/self-closing-comp': 'warn',
    'react/no-unstable-nested-components': [
      'warn',
      { allowAsProps: true, customValidators: ['formatter'] },
    ],
    'rulesdir/typography': 'warn',
    'rulesdir/prefer-if-statement': 'warn',
    // https://github.com/eslint/eslint/issues/16954
    // https://github.com/eslint/eslint/issues/16953
    'no-loop-func': 'off',
    // Do don't need this as we're using TypeScript
    'react/prop-types': 'off',

    'react/react-in-jsx-scope': 'off',
    'react/jsx-curly-brace-presence': 'warn',
    'object-shorthand': ['warn', 'properties'],
    'import/extensions': [
      'never',
      {
        json: 'always',
      },
    ],
    'import/no-useless-path-segments': 'warn',
    'import/no-duplicates': ['warn', { 'prefer-inline': true }],
    'import/no-unused-modules': ['warn', { unusedExports: true }],
        alphabetize: {
          caseInsensitive: true,
        groups: [
          'builtin', // Built-in types are first
          'external',
        ],
        'newlines-between': 'always',
        pathGroups: [
          // Enforce that React (and react-related packages) is the first import
          { group: 'builtin', pattern: 'react?(-*)', position: 'before' },
          // Separate imports from Actual from "real" external imports
          {
            group: 'external',
            pattern: 'loot-{core,design}/**/*',
        pathGroupsExcludedImportTypes: ['react'],
      },
    ],
    'no-restricted-syntax': [
      {
        // forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
        selector:
          ":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
        message:
          'Using default React import is discouraged, please use named exports directly instead.',
      },
Neil's avatar
Neil committed
        // forbid <a> in favor of <Link>
        selector: 'JSXOpeningElement[name.name="a"]',
Neil's avatar
Neil committed
        message: 'Using <a> is discouraged, please use <Link> instead.',
    'no-restricted-imports': [
      { patterns: [...restrictedImportPatterns, ...restrictedImportColors] },
    ],
    '@typescript-eslint/ban-ts-comment': [
      'error',
      { 'ts-ignore': 'allow-with-description' },
    ],

    // Rules disable during TS migration
    '@typescript-eslint/no-var-requires': 'off',
    'prefer-spread': 'off',
    '@typescript-eslint/no-empty-function': 'off',
    '@typescript-eslint/no-require-imports': 'off',
    'import/no-default-export': 'warn',
    {
      files: ['**/*.ts?(x)'],
      parser: '@typescript-eslint/parser',
      parserOptions: {
        ecmaVersion: 2018,
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },

        // typescript-eslint specific options
        warnOnUnsupportedTypeScriptVersion: true,
      },
      plugins: ['@typescript-eslint'],
      // If adding a typescript-eslint version of an existing ESLint rule,
      // make sure to disable the ESLint rule here.
      rules: {
        // TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
        'default-case': 'off',
        // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
        'no-dupe-class-members': 'off',
        // 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
        'no-undef': 'off',

        // Add TypeScript specific rules (and turn off ESLint equivalents)
        '@typescript-eslint/consistent-type-assertions': 'warn',
        'no-array-constructor': 'off',
        '@typescript-eslint/no-array-constructor': 'warn',
        'no-redeclare': 'off',
        '@typescript-eslint/no-redeclare': 'warn',
        'no-use-before-define': 'off',
        '@typescript-eslint/no-use-before-define': [
          'warn',
          {
            functions: false,
            classes: false,
            variables: false,
            typedefs: false,
          },
        ],
        'no-unused-expressions': 'off',
        '@typescript-eslint/no-unused-expressions': [
          'error',
          {
            allowShortCircuit: true,
            allowTernary: true,
            allowTaggedTemplates: true,
          },
        ],
        'no-useless-constructor': 'off',
        '@typescript-eslint/no-useless-constructor': 'warn',
      },
    },
      files: ['.eslintrc.js', './**/.eslintrc.js'],
      parserOptions: { project: null },
      rules: {
        '@typescript-eslint/consistent-type-exports': 'off',
      },
    },
    {
      files: [
        './packages/desktop-client/**/*.{ts,tsx}',
        './packages/loot-core/src/client/**/*.{ts,tsx}',
      ],
      rules: {
        // enforce type over interface
        '@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
        // enforce import type
        '@typescript-eslint/consistent-type-imports': [
          { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
        ],
        '@typescript-eslint/no-restricted-types': [
          {
            types: {
              // forbid FC as superflous
              FunctionComponent: { message: ruleFCMsg },
              FC: { message: ruleFCMsg },
            },
          },
        ],
      },
    },
    {
      files: ['./packages/desktop-client/**/*'],
      excludedFiles: [
        './packages/desktop-client/src/hooks/useNavigate.{ts,tsx}',
      ],
      rules: {
        'no-restricted-imports': [
          'warn',
          {
            patterns: [
              {
                group: ['react-router-dom'],
                importNames: ['useNavigate'],
                message: 'Please use Actual’s useNavigate() hook instead.',
              },
            ],
          },
        ],
      },
    },
    {
      files: ['./packages/loot-core/src/**/*'],
      rules: {
        'no-restricted-imports': [
              ...restrictedImportPatterns,
              {
                group: ['loot-core/**'],
                message:
                  'Please use relative imports in loot-core instead of importing from `loot-core/*`',
              },
            ],
          },
        ],
      },
    },
        'packages/loot-core/src/types/**/*',
        'packages/loot-core/src/client/state-types/**/*',
        '**/icons/**/*',
        '**/{mocks,__mocks__}/**/*',
        // can't correctly resolve usages
        '**/*.{testing,electron,browser,web,api}.ts',
      ],
      rules: { 'import/no-unused-modules': 'off' },
Neil's avatar
Neil committed
        './packages/desktop-client/src/style/index.*',
        './packages/desktop-client/src/style/palette.*',
      ],
      rules: {
        'no-restricted-imports': ['off', { patterns: restrictedImportColors }],
      },
    },
        './packages/api/migrations/*',
        './packages/loot-core/migrations/*',
        'import/no-default-export': 'off',
    {
      // TODO: fix the issues in these files
      files: [
        './packages/desktop-client/src/components/accounts/Account.jsx',
        './packages/desktop-client/src/components/accounts/MobileAccount.jsx',
        './packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
        './packages/desktop-client/src/components/App.tsx',
        './packages/desktop-client/src/components/budget/BudgetCategories.jsx',
        './packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
        './packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
        './packages/desktop-client/src/components/budget/index.tsx',
        './packages/desktop-client/src/components/budget/MobileBudget.tsx',
        './packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
        './packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
        './packages/desktop-client/src/components/common/Menu.tsx',
        './packages/desktop-client/src/components/FinancesApp.tsx',
        './packages/desktop-client/src/components/GlobalKeys.ts',
        './packages/desktop-client/src/components/LoggedInUser.tsx',
        './packages/desktop-client/src/components/manager/ManagementApp.jsx',
        './packages/desktop-client/src/components/manager/subscribe/common.tsx',
        './packages/desktop-client/src/components/ManageRules.tsx',
        './packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
        './packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
        './packages/desktop-client/src/components/Modals.tsx',
        './packages/desktop-client/src/components/modals/EditRule.jsx',
        './packages/desktop-client/src/components/modals/ImportTransactions.jsx',
        './packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
        './packages/desktop-client/src/components/Notifications.tsx',
        './packages/desktop-client/src/components/payees/ManagePayees.jsx',
        './packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
        './packages/desktop-client/src/components/payees/PayeeTable.tsx',
        './packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
        './packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
        './packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
        './packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
        './packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
        './packages/desktop-client/src/components/reports/SaveReportName.tsx',
        './packages/desktop-client/src/components/reports/useReport.ts',
        './packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
        './packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
        './packages/desktop-client/src/components/select/DateSelect.tsx',
        './packages/desktop-client/src/components/sidebar/Tools.tsx',
        './packages/desktop-client/src/components/sort.tsx',
        './packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
        './packages/desktop-client/src/components/table.tsx',
        './packages/desktop-client/src/components/Titlebar.tsx',
        './packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
        './packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
        './packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
        './packages/desktop-client/src/components/transactions/TransactionList.jsx',
        './packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
        './packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
        './packages/desktop-client/src/hooks/useAccounts.ts',
        './packages/desktop-client/src/hooks/useCategories.ts',
        './packages/desktop-client/src/hooks/usePayees.ts',
        './packages/desktop-client/src/hooks/useProperFocus.tsx',
        './packages/desktop-client/src/hooks/useSelected.tsx',
        './packages/loot-core/src/client/query-hooks.tsx',
      ],
      rules: {
        'react-hooks/exhaustive-deps': 'off',
      },
    },
    {
      files: [
        '.eslintrc.js',
        '*.test.js',
        '*.test.ts',
        '*.test.jsx',
        '*.test.tsx',
      ],
      rules: {
        'rulesdir/typography': 'off',
      },
    },
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
      },