@kununu/eslint-config
Version:
kununu's ESLint config
317 lines (310 loc) • 11.7 kB
JavaScript
import babelParser from '@babel/eslint-parser';
import babelPlugin from '@babel/eslint-plugin';
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import granularSelectorsPlugin from 'eslint-plugin-granular-selectors';
import pluginJest from 'eslint-plugin-jest';
import jestDomPlugin from 'eslint-plugin-jest-dom';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import lodashPlugin from 'eslint-plugin-lodash';
import perfectionistPlugin from 'eslint-plugin-perfectionist';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import sonarjs from 'eslint-plugin-sonarjs';
import testingLibraryPlugin from 'eslint-plugin-testing-library';
import {defineConfig, globalIgnores} from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig([
js.configs.recommended,
jsxA11yPlugin.flatConfigs.recommended,
perfectionistPlugin.configs['recommended-natural'],
reactHooksPlugin.configs.flat.recommended,
reactPlugin.configs.flat.recommended,
reactPlugin.configs.flat['jsx-runtime'],
sonarjs.configs.recommended,
stylistic.configs.recommended,
// Scope the TypeScript rules to TS files only; .js/.jsx use the Babel parser
// below and would otherwise run type-aware rules under the wrong parser.
...tseslint.configs.recommended.map(config => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
})),
globalIgnores([
'**/.next/',
'**/__mocks__/',
'**/__snapshots__/',
'**/build/',
'**/coverage/',
'**/dist/',
'**/node_modules/',
'**/out/',
'.git/',
'jest.config.js',
'newrelic.js',
'next.config.js',
]),
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
projectService: true,
},
},
rules: {
'@typescript-eslint/no-require-imports': 'error',
// Allow intentionally-unused identifiers prefixed with `_`.
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-use-before-define': 'error',
},
},
{
files: ['**/*.js', '**/*.jsx'],
languageOptions: {
parser: babelParser,
parserOptions: {
requireConfigFile: false,
},
},
plugins: {
babel: babelPlugin,
},
},
// Test-only configs. Spread as separate entries so their plugins/rules
// aren't clobbered, and scope them to spec files (incl. testing-library,
// which previously applied to every file).
{...pluginJest.configs['flat/recommended'], files: ['**/*.spec.*']},
{...jestDomPlugin.configs['flat/recommended'], files: ['**/*.spec.*']},
{...testingLibraryPlugin.configs['flat/dom'], files: ['**/*.spec.*']},
{
files: ['**/*.spec.*'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
// Tests legitimately require() dynamic mocks / json fixtures.
'@typescript-eslint/no-require-imports': 'off',
// Recognise assertions made through `assert*` helper functions, not just
// direct expect() calls.
'jest/expect-expect': ['warn', {assertFunctionNames: ['expect', 'assert*']}],
// kununu stores shared test fixtures under __mocks__ and imports them
// directly; this rule targets jest auto-mocks, not that convention.
'jest/no-mocks-import': 'off',
'react/display-name': 'off',
// Prop-types are redundant in a TypeScript codebase.
'react/prop-types': 'off',
// Test fixtures often contain password-like strings (mock query strings,
// tokens) that aren't real credentials; keep the rule for source code.
'sonarjs/no-hardcoded-passwords': 'off',
// Direct DOM access is common and acceptable in tests.
'testing-library/no-node-access': 'off',
'testing-library/render-result-naming-convention': 'off',
},
},
{
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
plugins: {
'granular-selectors': granularSelectorsPlugin,
lodash: lodashPlugin,
},
rules: {
// Prevent empty lines in arrays
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-parens': ['error', 'as-needed'],
'@stylistic/brace-style': ['error', '1tbs'],
'@stylistic/max-len': ['error', {
code: 120,
ignoreComments: true, // Ignore all comments
ignoreRegExpLiterals: true, // Ignore lines containing regex patterns
ignoreStrings: true, // Ignore lines with long strings (SVG paths, long text content)
ignoreTemplateLiterals: true, // Ignore template literals
ignoreTrailingComments: true, // Ignore trailing comments on lines with code
ignoreUrls: true, // Ignore lines containing URLs
}],
'@stylistic/member-delimiter-style': ['error', {
multiline: {
delimiter: 'semi',
requireLast: true,
},
overrides: {
interface: {
multiline: {
delimiter: 'semi',
requireLast: true,
},
},
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
}],
// Prevent multiple consecutive empty lines but allow single ones
'@stylistic/no-multiple-empty-lines': ['error', {
max: 1,
maxBOF: 0,
maxEOF: 0,
}],
// For object destructuring patterns
'@stylistic/object-curly-newline': ['error', {consistent: true}],
// Prevent empty lines inside object literals and destructuring
'@stylistic/object-curly-spacing': ['error', 'never'],
'@stylistic/operator-linebreak': ['error', 'after', {overrides: {'|': 'before'}}],
'@stylistic/padded-blocks': ['error', 'never'],
'@stylistic/padding-line-between-statements': ['error',
// Allow any spacing between imports (to allow grouping and other import rules)
{blankLine: 'any', next: 'import', prev: 'import'},
// Always require blank line after imports when followed by non-imports
{blankLine: 'always', next: '*', prev: 'import'},
// Re-allow consecutive imports with no blank line. Must stay AFTER the
// rule above (last match wins) or every adjacent import is forced apart.
{blankLine: 'any', next: 'import', prev: 'import'},
// Allow any spacing between variable declarations (before and between)
{blankLine: 'any', next: ['const', 'let', 'var'], prev: '*'},
// Always require blank line after variable declarations when followed by non-variables
{blankLine: 'always', next: '*', prev: ['const', 'let', 'var']},
// But allow variables to be followed by variables without forcing blank line (override above)
{blankLine: 'any', next: ['const', 'let', 'var'], prev: ['const', 'let', 'var']},
// Always require blank line before return statements
{blankLine: 'always', next: 'return', prev: '*'},
// Always require blank line before case and default statements
{blankLine: 'always', next: '*', prev: ['case', 'default']},
// Allow blank lines between JSX elements (expression statements)
{blankLine: 'any', next: 'expression', prev: 'expression'}],
'@stylistic/quote-props': ['error', 'as-needed'],
'@stylistic/semi': ['error', 'always'],
'@stylistic/switch-colon-spacing': 'error',
'granular-selectors/granular-selectors': ['error', {
include: ['use.*Selector.*', 'use.*Store.*'],
}],
'lodash/import-scope': [2, 'method'],
'no-console': 'warn',
'no-param-reassign': ['error', {props: false}],
'no-restricted-imports': [
'error',
{
paths: [
{
importNames: ['default'],
message: '\n We want to avoid importing react directly. Please import individual hooks and types from the react module instead.',
name: 'react',
},
],
},
],
'no-use-before-define': 'off',
'perfectionist/sort-imports': [
'error',
{
customGroups: [
{
elementNamePattern: ['^react$'],
groupName: 'react',
},
{
elementNamePattern: [
'^actions/.+',
'^client/.+',
'^components/.+',
'^contexts/.+',
'^genericTypes/.+',
'^hooks/.+',
'^images/.+',
'^mocks/.+',
'^pages/.+',
'^server/.+',
'^slices/.+',
'^src/.+',
'^state/.+',
'^tracking/.+',
'^types/.+',
'^utils/.+',
],
groupName: 'alias',
},
],
groups: [
'react',
'type-import',
['type-builtin', 'type-external'],
'type-internal',
'type-parent',
'type-sibling',
'type-index',
['value-builtin', 'value-external'],
'value-internal',
'alias',
'value-parent',
'value-sibling',
'value-index',
'ts-equals-import',
'unknown',
],
internalPattern: ['^~/.+', '^@kununu/.+'],
type: 'natural',
},
],
'perfectionist/sort-modules': ['error', {type: 'usage'}],
'perfectionist/sort-union-types': ['error', {
groups: [
'conditional',
'function',
'import',
'intersection',
'keyword',
'literal',
'named',
'object',
'operator',
'tuple',
'union',
'nullish',
],
}],
'prefer-template': 'error',
'react-hooks/exhaustive-deps': 'warn',
// React Compiler rules: advisory for now. They flag patterns the compiler
// can't optimise, but the current codebases use them widely; keep as
// warnings rather than blocking errors.
'react-hooks/immutability': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/use-memo': 'warn',
'react/jsx-closing-bracket-location': ['error', 'tag-aligned'],
// More restrictive JSX rules that are auto-fixable
'react/jsx-curly-spacing': ['error', 'never'],
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
// Control JSX prop formatting more precisely
'react/jsx-max-props-per-line': ['error', {maximum: 1, when: 'multiline'}],
// Keep the non-auto-fixable rule to detect the issue
'react/jsx-props-no-multi-spaces': 'error',
// Use jsx-tag-spacing for what it can auto-fix
'react/jsx-tag-spacing': ['error', {
afterOpening: 'never',
beforeClosing: 'never',
beforeSelfClosing: 'always',
closingSlash: 'never',
}],
'sonarjs/deprecation': ['warn'],
// TypeScript already infers/checks return types; this duplicates it.
'sonarjs/function-return-type': 'off',
'sonarjs/todo-tag': ['warn'],
},
},
]);