agent-ecosystem/eslint.config.js

591 lines
18 KiB
JavaScript

import { defineConfig, globalIgnores } from 'eslint/config';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import tailwindcss from 'eslint-plugin-tailwindcss';
import sonarjs from 'eslint-plugin-sonarjs';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import importPlugin from 'eslint-plugin-import';
import security from 'eslint-plugin-security';
import boundaries from 'eslint-plugin-boundaries';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
import globals from 'globals';
export default defineConfig([
// Global ignores
globalIgnores([
'dist/**',
'dist-electron/**',
'build/**',
'node_modules/**',
'*.config.js',
'*.config.cjs',
'*.config.ts',
'out/**',
]),
// Base ESLint recommended rules
js.configs.recommended,
// TypeScript-ESLint recommended with type checking + stylistic
// Using recommended (not strict) for a balanced approach
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
// SonarJS - Code quality and bug detection rules
sonarjs.configs.recommended,
// Security - Catch common security mistakes in AI-generated code
security.configs.recommended,
// TypeScript parser options for type-aware linting
{
name: 'typescript-parser-options',
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// Import plugin configuration - Main/Preload (uses tsconfig.node.json)
{
name: 'import-plugin-main',
files: ['src/main/**/*.ts', 'src/preload/**/*.ts'],
plugins: {
import: importPlugin,
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.node.json',
},
},
},
rules: {
'import/no-cycle': ['error', { maxDepth: 3, ignoreExternal: true }],
'import/no-unresolved': 'error',
'import/no-default-export': 'warn',
},
},
// Import plugin configuration - Renderer (uses tsconfig.json)
{
name: 'import-plugin-renderer',
files: ['src/renderer/**/*.{ts,tsx}'],
plugins: {
import: importPlugin,
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
},
rules: {
'import/no-cycle': ['error', { maxDepth: 3, ignoreExternal: true }],
'import/no-unresolved': 'error',
'import/no-default-export': 'warn',
},
},
// Module boundaries - Enforce Electron three-process architecture
{
name: 'module-boundaries',
files: ['src/**/*.{js,jsx,ts,tsx}'],
plugins: {
boundaries: boundaries,
},
settings: {
'boundaries/elements': [
{ type: 'main', pattern: 'src/main/**', mode: 'folder' },
{ type: 'preload', pattern: 'src/preload/**', mode: 'folder' },
{ type: 'renderer', pattern: 'src/renderer/**', mode: 'folder' },
{ type: 'shared', pattern: 'src/shared/**', mode: 'folder' },
],
'boundaries/ignore': ['**/*.test.ts', '**/*.spec.ts'],
},
rules: {
// Enforce strict module boundaries for Electron architecture
'boundaries/element-types': [
'error',
{
default: 'disallow',
rules: [
// Renderer can only import from renderer and shared
{ from: 'renderer', allow: ['renderer', 'shared'] },
// Main process can only import from main and shared
{ from: 'main', allow: ['main', 'shared'] },
// Preload can only import from preload and shared
{ from: 'preload', allow: ['preload', 'shared'] },
// Shared can import from shared and main (for type re-exports)
{ from: 'shared', allow: ['shared', 'main'] },
],
},
],
// Prevent importing private modules
'boundaries/no-private': 'error',
},
},
// ESLint Comments
{
name: 'eslint-comments',
files: ['src/**/*.{js,jsx,ts,tsx}'],
plugins: {
'@eslint-community/eslint-comments': eslintComments,
},
rules: {
// Prevents blanket-disabling rules
'@eslint-community/eslint-comments/no-unlimited-disable': 'error',
// Require description for disable comments
'@eslint-community/eslint-comments/require-description': [
'error',
{ ignore: [] },
],
// Re-enable rules after disabling
'@eslint-community/eslint-comments/disable-enable-pair': 'error',
// No duplicate disable comments
'@eslint-community/eslint-comments/no-duplicate-disable': 'error',
// Unused disable comments
'@eslint-community/eslint-comments/no-unused-disable': 'error',
},
},
// Import sorting for all JS/TS files
{
name: 'import-sorting',
files: ['src/**/*.{js,jsx,ts,tsx}'],
plugins: {
'simple-import-sort': simpleImportSort,
},
rules: {
'simple-import-sort/imports': [
'error',
{
groups: [
// Side effect imports (e.g., import './styles.css')
['^\\u0000'],
// Node.js builtins (fs, path, etc.)
['^node:'],
// React and related packages
['^react', '^react-dom'],
// External packages from node_modules
['^@?\\w'],
// Internal aliases (@/ paths)
['^@/'],
// Parent imports (../)
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Same-folder imports (./)
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Type imports
['^.+\\u0000$'],
],
},
],
'simple-import-sort/exports': 'error',
},
},
// Main process (Electron Node.js)
{
name: 'electron-main',
files: ['src/main/**/*.ts'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
// Allow console in main process for logging
'no-console': 'off',
},
},
// Preload script (Electron bridge)
{
name: 'electron-preload',
files: ['src/preload/**/*.ts'],
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
},
},
// Renderer process (React + A11y + Tailwind)
{
name: 'renderer-react',
files: ['src/renderer/**/*.{ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
},
},
plugins: {
react: reactPlugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'jsx-a11y': jsxA11y,
tailwindcss: tailwindcss,
},
settings: {
react: {
version: 'detect',
},
tailwindcss: {
// Tailwind config path (relative to cwd)
config: 'tailwind.config.js',
// Allow custom classnames (e.g., from CSS modules)
callees: ['classnames', 'clsx', 'cn'],
},
},
rules: {
// React recommended rules
...reactPlugin.configs.recommended.rules,
// JSX runtime (React 17+) - no need to import React
...reactPlugin.configs['jsx-runtime'].rules,
// React Hooks rules
...reactHooks.configs.recommended.rules,
// Accessibility rules (recommended)
...jsxA11y.configs.recommended.rules,
// Tailwind CSS rules
...tailwindcss.configs.recommended.rules,
// React Refresh for HMR
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// Disable prop-types since we use TypeScript
'react/prop-types': 'off',
// A11y rule adjustments for this project
// Allow click handlers on divs when keyboard handlers also present
'jsx-a11y/click-events-have-key-events': 'warn',
'jsx-a11y/no-static-element-interactions': 'warn',
// Allow autofocus for search inputs in desktop apps
'jsx-a11y/no-autofocus': 'off',
// Tailwind CSS rule adjustments
// Warn on class order (Prettier plugin handles sorting)
'tailwindcss/classnames-order': 'off', // Prettier plugin handles this
// Warn on conflicting classes
'tailwindcss/no-contradicting-classname': 'error',
// Warn on custom classnames that don't exist
'tailwindcss/no-custom-classname': 'warn',
// === React-Specific Rules ===
// Consistent component definition
'react/function-component-definition': [
'error',
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
// Strengthen exhaustive-deps
'react-hooks/exhaustive-deps': 'error',
// Prevent prop spreading
'react/jsx-props-no-spreading': [
'warn',
{
exceptions: ['input', 'button', 'Input', 'Button', 'textarea', 'select'],
},
],
// Ensure key props
'react/jsx-key': [
'error',
{
checkFragmentShorthand: true,
checkKeyMustBeforeSpread: true,
},
],
// Prevent unnecessary fragments
'react/jsx-no-useless-fragment': 'warn',
// Self-closing components for consistency
'react/self-closing-comp': [
'error',
{
component: true,
html: true,
},
],
},
},
// Test files
{
name: 'test-files',
files: ['test/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
languageOptions: {
globals: {
...globals.node,
},
parserOptions: {
projectService: false,
project: './tsconfig.test.json',
},
},
rules: {
// Relax TypeScript strict rules for tests
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/unbound-method': 'off',
// Relax function/export rules for tests
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// Relax naming conventions for tests (allow describe, it, expect patterns)
'@typescript-eslint/naming-convention': 'off',
// Allow magic numbers in tests
'sonarjs/no-hardcoded-ip': 'off',
// Allow floating promises in tests (common with async test helpers)
'@typescript-eslint/no-floating-promises': 'off',
},
},
// Custom rule overrides for all TypeScript files
{
name: 'custom-rules',
files: ['src/**/*.{ts,tsx}'],
rules: {
// === Core JavaScript rules ===
'prefer-const': 'error',
'no-var': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
// === TypeScript Import/Export rules ===
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/consistent-type-exports': [
'error',
{ fixMixedExportsWithInlineTypeSpecifier: true },
],
// === Unused variables ===
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
// === Relaxed strict rules for practical use ===
// Allow empty functions (useful for callbacks and stubs)
'@typescript-eslint/no-empty-function': 'off',
// Allow numbers/booleans in template literals (common pattern)
'@typescript-eslint/restrict-template-expressions': [
'error',
{
allowNumber: true,
allowBoolean: true,
allowNullish: false,
},
],
// Allow async functions without await (IPC handlers often need this)
'@typescript-eslint/require-await': 'off',
// Allow floating promises in event handlers (common in Electron)
'@typescript-eslint/no-floating-promises': [
'error',
{
ignoreVoid: true,
ignoreIIFE: true,
},
],
// Allow promises in places that don't expect them (event handlers)
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
attributes: false,
arguments: false,
},
},
],
// Allow void expression in arrow functions shorthand
'@typescript-eslint/no-confusing-void-expression': [
'error',
{
ignoreArrowShorthand: true,
ignoreVoidOperator: true,
},
],
// Prefer nullish coalescing but don't error on logical or
'@typescript-eslint/prefer-nullish-coalescing': 'off',
// Allow inferrable types (style preference)
'@typescript-eslint/no-inferrable-types': 'off',
// === Anti-Hallucination Rules ===
// Explicit return types
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true,
},
],
// Explicit types for exported functions (minimum requirement)
'@typescript-eslint/explicit-module-boundary-types': 'warn',
// Prevent variable shadowing
'@typescript-eslint/no-shadow': 'error',
// === Naming Conventions ===
'@typescript-eslint/naming-convention': [
'warn',
// Imports can be camelCase or PascalCase (React, ReactDOM, App, etc.)
{
selector: 'import',
format: ['camelCase', 'PascalCase'],
},
// Default: variables and parameters in camelCase
{
selector: 'default',
format: ['camelCase'],
leadingUnderscore: 'allow',
},
// Static readonly class properties can be UPPER_CASE
{
selector: 'classProperty',
modifiers: ['static', 'readonly'],
format: ['camelCase', 'UPPER_CASE'],
},
// Variables: camelCase or UPPER_CASE for constants
{
selector: 'variable',
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allow',
},
// Functions: camelCase (includes type guards like isXxx, builders like buildXxx)
{
selector: 'function',
format: ['camelCase', 'PascalCase'],
},
// Parameters: camelCase, allow leading underscore for unused
{
selector: 'parameter',
format: ['camelCase'],
leadingUnderscore: 'allow',
},
// Types and interfaces in PascalCase
{
selector: 'typeLike',
format: ['PascalCase'],
},
// Interfaces should NOT start with I (modern convention)
{
selector: 'interface',
format: ['PascalCase'],
custom: { regex: '^I[A-Z]', match: false },
},
// Enum members in PascalCase or UPPER_CASE
{
selector: 'enumMember',
format: ['PascalCase', 'UPPER_CASE'],
},
// Object literal properties: allow any format (for API compatibility)
{
selector: 'objectLiteralProperty',
format: null,
},
// Type properties: allow any format (for type definitions matching APIs)
{
selector: 'typeProperty',
format: null,
},
],
// === Import Restrictions ===
// Note: boundaries/element-types handles main/renderer separation
'no-restricted-imports': [
'error',
{
patterns: [
// Prevent deep relative imports - use @/ aliases
{
group: ['../**/..'],
message: 'Avoid deep relative imports, use @/ aliases',
},
],
},
],
// === Mutation Prevention ===
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsFor: ['draft', 'acc', 'ctx', 'state', 'req', 'res'],
},
],
// === SonarJS rule adjustments ===
// Cognitive complexity - warn instead of error for gradual adoption
'sonarjs/cognitive-complexity': 'off',
// Allow some duplication in similar but not identical code
'sonarjs/no-duplicate-string': 'off',
// Relax for Electron IPC patterns (many similar switch cases)
'sonarjs/no-small-switch': 'off',
// Allow nested ternaries in JSX (common React pattern)
'sonarjs/no-nested-conditional': 'off',
// === Security rule adjustments (Code Protection) ===
// These catch common security mistakes
'security/detect-eval-with-expression': 'error',
// Disabled: This is a desktop file reader app - file system access is expected
'security/detect-non-literal-fs-filename': 'off',
// Disabled: Dynamic patterns are intentional in this app
'security/detect-non-literal-regexp': 'off',
// Disabled: Often false positives with typed code
'security/detect-object-injection': 'off',
'security/detect-child-process': 'warn',
'security/detect-non-literal-require': 'warn',
'security/detect-possible-timing-attacks': 'warn',
},
},
// === IMPORTANT: eslint-config-prettier MUST be LAST ===
// This disables all ESLint rules that conflict with Prettier
// Prettier handles formatting, ESLint handles code quality
eslintConfigPrettier,
]);