eslint-config-agent 3.0.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +592 -26
  3. package/configs/base-plugins.js +32 -0
  4. package/configs/config-files.js +19 -3
  5. package/configs/examples.js +12 -1
  6. package/configs/javascript.js +12 -4
  7. package/configs/length-rule-scope.js +27 -0
  8. package/configs/overrides.js +3 -14
  9. package/configs/test-files.js +40 -1
  10. package/configs/tsx.js +10 -0
  11. package/configs/typescript.js +44 -2
  12. package/exports/ddd.d.ts +10 -0
  13. package/exports/ddd.js +1 -3
  14. package/exports/incremental.d.ts +11 -0
  15. package/exports/incremental.js +39 -0
  16. package/exports/recommended-incremental.d.ts +11 -0
  17. package/exports/recommended-incremental.js +46 -0
  18. package/exports/recommended.d.ts +14 -0
  19. package/exports/recommended.js +110 -0
  20. package/exports/to-warnings.d.ts +20 -0
  21. package/exports/to-warnings.js +41 -0
  22. package/index.d.ts +18 -0
  23. package/index.js +883 -7
  24. package/package.json +46 -17
  25. package/plugins/index.js +6 -0
  26. package/rules/index.js +20 -22
  27. package/rules/no-inline-union-types/index.js +8 -6
  28. package/rules/no-process-env-properties/index.js +4 -4
  29. package/rules/no-record-literal-types/index.js +3 -3
  30. package/rules/plugin/import/index.js +41 -0
  31. package/rules/plugin/index.js +0 -2
  32. package/rules/plugin/n/index.js +16 -2
  33. package/rules/plugin/n/no-process-env/index.js +4 -4
  34. package/rules/plugin/react/index.js +150 -2
  35. package/rules/plugin/typescript-eslint/index.js +406 -0
  36. package/rules/require-spec-file-tsx/README.md +57 -0
  37. package/rules/require-spec-file-tsx/helpers.js +93 -0
  38. package/rules/require-spec-file-tsx/index.js +109 -0
  39. package/rules/plugin/jsx-a11y/index.js +0 -3
package/package.json CHANGED
@@ -1,21 +1,46 @@
1
1
  {
2
2
  "name": "eslint-config-agent",
3
- "version": "3.0.4",
3
+ "version": "3.1.0",
4
4
  "description": "ESLint configuration package with TypeScript support",
5
5
  "main": "index.js",
6
+ "types": "./index.d.ts",
6
7
  "type": "module",
7
8
  "exports": {
8
- ".": "./index.js",
9
- "./ddd": "./exports/ddd.js"
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./ddd": {
14
+ "types": "./exports/ddd.d.ts",
15
+ "default": "./exports/ddd.js"
16
+ },
17
+ "./recommended": {
18
+ "types": "./exports/recommended.d.ts",
19
+ "default": "./exports/recommended.js"
20
+ },
21
+ "./recommended-incremental": {
22
+ "types": "./exports/recommended-incremental.d.ts",
23
+ "default": "./exports/recommended-incremental.js"
24
+ },
25
+ "./incremental": {
26
+ "types": "./exports/incremental.d.ts",
27
+ "default": "./exports/incremental.js"
28
+ },
29
+ "./to-warnings": {
30
+ "types": "./exports/to-warnings.d.ts",
31
+ "default": "./exports/to-warnings.js"
32
+ },
33
+ "./package.json": "./package.json"
10
34
  },
11
35
  "files": [
12
36
  "index.js",
37
+ "index.d.ts",
13
38
  "README.md",
14
39
  "CHANGELOG.md",
15
40
  "configs/",
16
41
  "plugins/",
17
42
  "exports/",
18
- "rules/**/index.js",
43
+ "rules/**/*.js",
19
44
  "!rules/**/examples/",
20
45
  "!rules/**/*.spec.js",
21
46
  "!rules/**/*.test.js"
@@ -23,6 +48,14 @@
23
48
  "engines": {
24
49
  "node": ">=20.0.0"
25
50
  },
51
+ "peerDependencies": {
52
+ "typescript": ">=4.8.4"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "typescript": {
56
+ "optional": true
57
+ }
58
+ },
26
59
  "scripts": {
27
60
  "lint": "eslint .",
28
61
  "lint:test": "eslint test/",
@@ -32,6 +65,12 @@
32
65
  "test:invalid": "eslint test/invalid.tsx test/long-function.tsx test/jsx-extension-test.js",
33
66
  "test:hooks": "eslint test/react-hooks-rules.tsx",
34
67
  "test:imports": "eslint test/import-export-rules.ts",
68
+ "test:recommended": "node scripts/test-recommended.js",
69
+ "test:recommended-incremental": "node scripts/test-recommended-incremental.js",
70
+ "test:exports": "node scripts/test-exports.js",
71
+ "test:pack": "node scripts/verify-pack.js",
72
+ "test:incremental": "node scripts/test-incremental.js",
73
+ "test:types": "tsc -p test/types/tsconfig.json",
35
74
  "test:edge": "eslint test/edge-cases.tsx",
36
75
  "test:performance": "eslint test/performance-test.tsx",
37
76
  "test:comprehensive": "node scripts/test-runner.js",
@@ -80,28 +119,16 @@
80
119
  "devDependencies": {
81
120
  "@commitlint/cli": "^20.5.3",
82
121
  "@commitlint/config-conventional": "^20.5.3",
83
- "@eslint/eslintrc": "^3.3.1",
84
- "@eslint/js": "^9.37.0",
85
122
  "@release-it/conventional-changelog": "^11.0.1",
86
- "@typescript-eslint/eslint-plugin": "^8.46.1",
87
- "@typescript-eslint/parser": "^8.46.1",
88
123
  "@typescript-eslint/rule-tester": "^8.60.1",
89
124
  "cspell": "^9.8.0",
90
- "eslint": "^9.37.0",
91
- "eslint-plugin-class-export": "^1.0.1",
92
- "eslint-plugin-import": "^2.32.0",
93
- "eslint-plugin-n": "^17.23.1",
94
- "eslint-plugin-preact": "^0.1.0",
95
- "eslint-plugin-react": "^7.37.5",
96
- "eslint-plugin-react-hooks": "^5.2.0",
97
- "eslint-plugin-security": "^3.0.1",
98
125
  "husky": "^9.1.7",
99
126
  "knip": "^6.15.0",
100
127
  "lint-staged": "^16.4.0",
101
128
  "prettier": "^3.8.3",
102
129
  "release-it": "^20.2.0",
103
130
  "storybook": "^10.4.2",
104
- "typescript-eslint": "^8.46.1"
131
+ "typescript": "^6.0.3"
105
132
  },
106
133
  "dependencies": {
107
134
  "@eslint/eslintrc": "^3.3.5",
@@ -127,6 +154,8 @@
127
154
  "eslint-plugin-single-export": "^1.1.2",
128
155
  "eslint-plugin-storybook": "^10.4.2",
129
156
  "eslint-plugin-switch-case": "^4.0.0",
157
+ "eslint-plugin-unicorn": "^65.0.1",
158
+ "eslint-plugin-unused-imports": "^4.4.1",
130
159
  "globals": "^17.6.0",
131
160
  "typescript-eslint": "^8.60.1"
132
161
  }
package/plugins/index.js CHANGED
@@ -18,7 +18,10 @@ import errorPlugin from 'eslint-plugin-error'
18
18
  import defaultPlugin from 'eslint-plugin-default'
19
19
  import dddPlugin from 'eslint-plugin-ddd'
20
20
  import preactPlugin from 'eslint-plugin-preact'
21
+ import unusedImportsPlugin from 'eslint-plugin-unused-imports'
22
+ import unicornPlugin from 'eslint-plugin-unicorn'
21
23
  import { noDefaultClassExportRule } from '../rules/no-default-class-export/index.js'
24
+ import { requireSpecFileTsxRule } from '../rules/require-spec-file-tsx/index.js'
22
25
 
23
26
  // Centralized plugin configuration
24
27
  export const plugins = {
@@ -35,9 +38,12 @@ export const plugins = {
35
38
  default: defaultPlugin,
36
39
  ddd: dddPlugin,
37
40
  preact: preactPlugin,
41
+ 'unused-imports': unusedImportsPlugin,
42
+ unicorn: unicornPlugin,
38
43
  custom: {
39
44
  rules: {
40
45
  'no-default-class-export': noDefaultClassExportRule,
46
+ 'require-spec-file-tsx': requireSpecFileTsxRule,
41
47
  },
42
48
  },
43
49
  }
package/rules/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  // Core rule configurations
9
- import noTrailingSpacesConfig from './no-trailing-spaces/index.js'
9
+ import { noTrailingSpacesConfig } from './no-trailing-spaces/index.js'
10
10
  import {
11
11
  maxFunctionLinesWarning,
12
12
  maxFunctionLinesError,
@@ -17,7 +17,7 @@ import {
17
17
  } from './max-file-lines/index.js'
18
18
 
19
19
  // Custom restricted syntax rules
20
- import { noProcessEnvPropertiesConfig } from './no-process-env-properties/index.js'
20
+ import { noProcessEnvironmentPropertiesConfig } from './no-process-env-properties/index.js'
21
21
  import { noTypeAssertionsConfig } from './no-type-assertions/index.js'
22
22
  import { noExportSpecifiersConfig } from './no-empty-exports/index.js'
23
23
  import { noDefaultClassExportRules } from './no-default-class-export/index.js'
@@ -42,7 +42,7 @@ const allRules = {
42
42
  maxFileLinesError,
43
43
 
44
44
  // Custom restricted syntax rules
45
- noProcessEnvPropertiesConfig,
45
+ noProcessEnvironmentPropertiesConfig,
46
46
  noTypeAssertionsConfig,
47
47
  noExportSpecifiersConfig,
48
48
  noDefaultClassExportRules,
@@ -61,27 +61,25 @@ const allRules = {
61
61
  export default allRules
62
62
 
63
63
  // Named exports for backward compatibility
64
+
65
+ export { noTrailingSpacesConfig } from './no-trailing-spaces/index.js'
64
66
  export {
65
- // Core rule configurations
66
- noTrailingSpacesConfig,
67
67
  maxFunctionLinesWarning,
68
68
  maxFunctionLinesError,
69
+ } from './max-function-lines/index.js'
70
+ export {
69
71
  maxFileLinesWarning,
70
72
  maxFileLinesError,
71
-
72
- // Custom restricted syntax rules
73
- noProcessEnvPropertiesConfig,
74
- noTypeAssertionsConfig,
75
- noExportSpecifiersConfig,
76
- noDefaultClassExportRules,
77
- noNullishCoalescingConfig,
78
- switchStatementsReturnTypeConfigs,
79
- switchCaseFunctionsReturnTypeConfigs,
80
- switchCaseExplicitReturnConfigs,
81
- noTrivialTypeAliasesConfigs,
82
- noInlineUnionTypesConfigs,
83
-
84
- // Plugin rule configurations
85
- pluginRules,
86
- typescriptEslintRules,
87
- }
73
+ } from './max-file-lines/index.js'
74
+ export { noProcessEnvironmentPropertiesConfig } from './no-process-env-properties/index.js'
75
+ export { noTypeAssertionsConfig } from './no-type-assertions/index.js'
76
+ export { noExportSpecifiersConfig } from './no-empty-exports/index.js'
77
+ export { noDefaultClassExportRules } from './no-default-class-export/index.js'
78
+ export { noNullishCoalescingConfig } from './nullish-coalescing/index.js'
79
+ export { switchStatementsReturnTypeConfigs } from './switch-statements-return-type/index.js'
80
+ export { switchCaseFunctionsReturnTypeConfigs } from './switch-case-functions-return-type/index.js'
81
+ export { switchCaseExplicitReturnConfigs } from './switch-case-explicit-return/index.js'
82
+ export { noTrivialTypeAliasesConfigs } from './no-trivial-type-aliases/index.js'
83
+ export { noInlineUnionTypesConfigs } from './no-inline-union-types/index.js'
84
+ export { pluginRules } from './plugin/index.js'
85
+ export { typescriptEslintRules } from './plugin/typescript-eslint/index.js'
@@ -7,7 +7,9 @@
7
7
  * Examples:
8
8
  * - ❌ function foo(status: 'active' | 'inactive') {}
9
9
  * - ❌ interface User { role: 'admin' | 'user'; }
10
+ * - ❌ interface User { id: string | number; }
10
11
  * - ❌ class Config { mode: 'dev' | 'prod'; }
12
+ * - ❌ class Config { value: string | number; }
11
13
  * - ✅ type Status = 'active' | 'inactive'; function foo(status: Status) {}
12
14
  * - ✅ type Role = 'admin' | 'user'; interface User { role: Role; }
13
15
  * - ✅ type Mode = 'dev' | 'prod'; class Config { mode: Mode; }
@@ -20,20 +22,20 @@ const rule = 'error'
20
22
  const selectorGeneral =
21
23
  'TSTypeAnnotation > TSUnionType:not(PropertyDefinition > .typeAnnotation > .typeAnnotation):not(TSPropertySignature > .typeAnnotation > .typeAnnotation)'
22
24
 
23
- // Interface properties with literal unions
25
+ // Interface properties with inline unions (literal or otherwise)
24
26
  const selectorInterfaceProperty =
25
- 'TSPropertySignature > TSTypeAnnotation > TSUnionType:has(TSLiteralType)'
27
+ 'TSPropertySignature > TSTypeAnnotation > TSUnionType'
26
28
 
27
- // Class properties with literal unions
29
+ // Class properties with inline unions (literal or otherwise)
28
30
  const selectorClassProperty =
29
- 'PropertyDefinition > TSTypeAnnotation > TSUnionType:has(TSLiteralType)'
31
+ 'PropertyDefinition > TSTypeAnnotation > TSUnionType'
30
32
 
31
33
  const messageGeneral =
32
34
  'Use a named type declaration instead of inline union types.'
33
35
  const messageProperty =
34
- 'Interface properties with literal unions should use a named type declaration.'
36
+ 'Interface properties with inline unions should use a named type declaration.'
35
37
  const messageClassProperty =
36
- 'Class properties with literal unions should use a named type declaration.'
38
+ 'Class properties with inline unions should use a named type declaration.'
37
39
 
38
40
  /**
39
41
  * Export the complete rule configurations for no-restricted-syntax
@@ -23,14 +23,14 @@ const message =
23
23
  /**
24
24
  * Export the complete rule configuration for no-restricted-syntax
25
25
  * Can be used in ESLint config as part of no-restricted-syntax rules:
26
- * "no-restricted-syntax": ["error", ...otherRules, noProcessEnvPropertiesConfig]
26
+ * "no-restricted-syntax": ["error", ...otherRules, noProcessEnvironmentPropertiesConfig]
27
27
  */
28
- const noProcessEnvPropertiesConfig = {
28
+ const noProcessEnvironmentPropertiesConfig = {
29
29
  selector,
30
30
  message,
31
31
  }
32
32
 
33
33
  // Consolidated exports
34
- export { rule, selector, message, noProcessEnvPropertiesConfig }
34
+ export { rule, selector, message, noProcessEnvironmentPropertiesConfig }
35
35
 
36
- export default noProcessEnvPropertiesConfig
36
+ export default noProcessEnvironmentPropertiesConfig
@@ -23,7 +23,7 @@
23
23
  const rule = 'error'
24
24
 
25
25
  // First selector: matches TSLiteralType descendants of first param (catches literals inside unions)
26
- const selectorNestedParams =
26
+ const selectorNestedParameters =
27
27
  'TSTypeReference[typeName.name="Record"] > TSTypeParameterInstantiation > .params:first-child TSLiteralType'
28
28
 
29
29
  // Second selector: matches direct TSLiteralType as first param
@@ -38,7 +38,7 @@ const message =
38
38
  * Can be used in ESLint config as part of no-restricted-syntax rules
39
39
  */
40
40
  const noRecordLiteralTypesNestedConfig = {
41
- selector: selectorNestedParams,
41
+ selector: selectorNestedParameters,
42
42
  message,
43
43
  }
44
44
 
@@ -58,7 +58,7 @@ const noRecordLiteralTypesConfigs = [
58
58
  // Consolidated exports
59
59
  export {
60
60
  rule,
61
- selectorNestedParams,
61
+ selectorNestedParameters,
62
62
  selectorFirstChild,
63
63
  message,
64
64
  noRecordLiteralTypesNestedConfig,
@@ -21,4 +21,45 @@ export const importRules = {
21
21
  'import/newline-after-import': 'off',
22
22
  'import/first': 'error',
23
23
  'import/prefer-default-export': 'off',
24
+ // Import hygiene: collapse duplicate import statements from the same module
25
+ // and forbid exporting mutable bindings (export let/var), which create
26
+ // shared mutable state across modules and are a common AI-generated footgun.
27
+ 'import/no-duplicates': 'error',
28
+ 'import/no-mutable-exports': 'error',
29
+ // Forbid circular import dependencies (A imports B imports A). Cycles cause
30
+ // order-dependent runtime bugs where a module reads a not-yet-initialized
31
+ // binding from its partner and silently sees `undefined`, defeat tree
32
+ // shaking, and are a reliable signal of tangled, hard-to-follow module
33
+ // boundaries. They are also a frequent AI-generated footgun, since an
34
+ // assistant editing one file cannot see the import graph it closes. Detect
35
+ // them statically. `maxDepth: Infinity` follows the full import graph so deep
36
+ // cycles are caught too; `ignoreExternal` skips traversal into node_modules
37
+ // for performance, since cycles inside dependencies are not the consumer's
38
+ // to fix.
39
+ 'import/no-cycle': ['error', { maxDepth: Infinity, ignoreExternal: true }],
40
+ // A module importing itself is always a mistake (usually a copy-paste or a
41
+ // bad auto-import) and produces a degenerate cycle; flag it explicitly.
42
+ 'import/no-self-import': 'error',
43
+ // Forbid empty named import blocks (`import {} from 'mod'`). An empty block
44
+ // is the residue of deleting the last named binding from an import: the
45
+ // statement now pulls in nothing yet still reads as if it imports names,
46
+ // which misleads readers and quietly keeps a dead dependency edge alive. A
47
+ // bare side-effect import (`import 'mod'`) — or removing the line — states
48
+ // the real intent. This is a frequent leftover from AI-assisted edits that
49
+ // rewrite an import list one binding at a time, putting it squarely in scope
50
+ // for this config's explicit-over-clever stance. The rule is auto-fixable.
51
+ 'import/no-empty-named-blocks': 'error',
52
+ // Forbid redundant segments in relative import paths, e.g. `./foo/../bar`
53
+ // (really `./bar`), `./../sibling` (really `../sibling`) or `./././mod`
54
+ // (really `./mod`). These resolve to the same module but read as if they
55
+ // point somewhere else, forcing a reviewer to mentally collapse the path to
56
+ // know what is actually imported, and they hide accidental directory
57
+ // traversals introduced by a bad auto-import or a copy-paste from another
58
+ // folder — a frequent AI-assisted-edit footgun and squarely the
59
+ // explicit-over-clever ground this config covers. The rule is auto-fixable,
60
+ // so adopting projects get a one-shot `--fix`. `commonjs: true` extends the
61
+ // check to `require()` calls; `noUselessIndex` is left at its default (off)
62
+ // so explicit `/index` references stay allowed and only genuinely redundant
63
+ // `../`/`./` segments are flagged.
64
+ 'import/no-useless-path-segments': ['error', { commonjs: true }],
24
65
  }
@@ -1,6 +1,5 @@
1
1
  import { securityRules } from './security/index.js'
2
2
  import { importRules } from './import/index.js'
3
- import { jsxA11yRules } from './jsx-a11y/index.js'
4
3
  import { reactRules } from './react/index.js'
5
4
  import { nRules } from './n/index.js'
6
5
  import { classExportRules } from './class-export/index.js'
@@ -8,7 +7,6 @@ import { classExportRules } from './class-export/index.js'
8
7
  export const pluginRules = {
9
8
  ...securityRules,
10
9
  ...importRules,
11
- ...jsxA11yRules,
12
10
  ...reactRules,
13
11
  ...nRules,
14
12
  ...classExportRules,
@@ -1,5 +1,19 @@
1
- import { noProcessEnvConfig } from './no-process-env/index.js'
1
+ import { noProcessEnvironmentConfig } from './no-process-env/index.js'
2
2
 
3
3
  export const nRules = {
4
- 'n/no-process-env': noProcessEnvConfig,
4
+ 'n/no-process-env': noProcessEnvironmentConfig,
5
+ // Require the `node:` protocol prefix on every import of a Node.js built-in
6
+ // module (e.g. `import fs from 'node:fs'` instead of `import fs from 'fs'`).
7
+ // The bare form (`'fs'`, `'path'`, `'crypto'`) is ambiguous: without the
8
+ // protocol, a reader (and a bundler) must consult package.json to confirm
9
+ // that the name is not a userland npm package shadowing the built-in. The
10
+ // `node:` prefix is unambiguous at a glance, is supported since Node 14.18.0
11
+ // (the minimum already required by this package's `engines` field), and is
12
+ // the form the package itself uses in all its own scripts and custom rules
13
+ // (e.g. `import { existsSync } from 'node:fs'`). Shipping the rule in the
14
+ // shared set means consuming projects inherit the same convention
15
+ // automatically. The rule is auto-fixable (`eslint --fix`), so adoption is
16
+ // free. It fires only when a built-in module is actually imported — browser
17
+ // and React code that imports nothing from Node.js is unaffected.
18
+ 'n/prefer-node-protocol': 'error',
5
19
  }
@@ -13,11 +13,11 @@ const options = {}
13
13
  /**
14
14
  * Export the complete rule configuration
15
15
  * Can be used in ESLint config as:
16
- * "n/no-process-env": noProcessEnvConfig
16
+ * "n/no-process-env": noProcessEnvironmentConfig
17
17
  */
18
- const noProcessEnvConfig = [rule, options]
18
+ const noProcessEnvironmentConfig = [rule, options]
19
19
 
20
20
  // Consolidated exports
21
- export { rule, options, noProcessEnvConfig }
21
+ export { rule, options, noProcessEnvironmentConfig }
22
22
 
23
- export default noProcessEnvConfig
23
+ export default noProcessEnvironmentConfig
@@ -1,5 +1,19 @@
1
1
  export const reactRules = {
2
- 'react/self-closing-comp': 'off',
2
+ // Require self-closing tags for JSX components and HTML elements that have
3
+ // no children. `<Button></Button>` and `<div></div>` become `<Button />` and
4
+ // `<div />` respectively. An explicit open/close tag pair is meaningful only
5
+ // when it contains children; without children it is pure noise — it adds an
6
+ // extra close tag the reader has to scan past to confirm nothing is inside,
7
+ // and it is exactly the verbose form AI assistants emit when scaffolding
8
+ // components from a template. The single-tag form states "this element has
9
+ // no children" directly, which is the explicit, low-noise style this config
10
+ // favors throughout. The rule is auto-fixable, so `eslint --fix` collapses
11
+ // every violating element in one pass. `component: true` covers React
12
+ // components (`<MyComp></MyComp>`) and `html: true` covers intrinsic
13
+ // elements (`<div></div>`, `<span></span>`), so both surfaces stay
14
+ // consistent. `oss-il` already enforces this manually on top of the base
15
+ // config; promoting it here removes that copy-paste.
16
+ 'react/self-closing-comp': ['error', { component: true, html: true }],
3
17
  'react/destructuring-assignment': 'off',
4
18
  'react/jsx-props-no-spreading': 'off',
5
19
  'react/button-has-type': 'off',
@@ -12,5 +26,139 @@ export const reactRules = {
12
26
  'react/require-default-props': 'off',
13
27
  'react/jsx-wrap-multilines': 'off',
14
28
  'react/jsx-closing-bracket-location': 'off',
15
- 'react/jsx-no-useless-fragment': 'off',
29
+ // Forbid fragments that wrap a single child: `<><MyComponent /></>` adds DOM
30
+ // overhead and a return-type mismatch (`React.ReactElement` vs `JSX.Element`)
31
+ // without buying anything. It is exactly the kind of dead-weight an AI
32
+ // assistant emits when scaffolding a component. `allowExpressions: false`
33
+ // also covers `<>{value}</>` — if you need a fragment solely for a JSX
34
+ // expression, wrap it in a parent instead. The rule is auto-fixable, so
35
+ // `eslint --fix` rewrites `<><X /></>` to `<X />` with zero manual work.
36
+ // oss-il already enables this by hand on top of the base config; promoting
37
+ // it here removes the per-repo copy-paste.
38
+ 'react/jsx-no-useless-fragment': ['error', { allowExpressions: false }],
39
+ // Guard against React's "leaked render" bug: a short-circuit like
40
+ // `{count && <List />}` renders the literal `0` (or `NaN`) when the left
41
+ // operand is a falsy *non-boolean*, and `{name && <h1>{name}</h1>}` renders
42
+ // an empty string the same way. React only skips rendering for `false`,
43
+ // `null` and `undefined`, so any other falsy value leaks straight into the
44
+ // DOM as visible text. That is a real rendering bug the type checker cannot
45
+ // catch (the JSX is perfectly typed) and exactly the shortcut AI assistants
46
+ // emit when conditionally rendering a list or label. Unlike the stylistic
47
+ // react rules turned off above, this is a correctness check, so it is turned
48
+ // on. `validStrategies: ['ternary', 'coerce']` forces the unambiguous form —
49
+ // an explicit `cond ? <X /> : null` or a boolean coercion `!!cond && <X />`
50
+ // — instead of the bare `&&` that leaks. The rule is auto-fixable, so
51
+ // consumers can adopt it with `eslint --fix`.
52
+ 'react/jsx-no-leaked-render': [
53
+ 'error',
54
+ { validStrategies: ['ternary', 'coerce'] },
55
+ ],
56
+ // Forbid using an array index as a React `key` (`items.map((item, i) => <Row
57
+ // key={i} />)`). A key is React's identity for a list child across renders;
58
+ // when it is the position rather than something stable about the item, React
59
+ // maps the wrong previous element onto the wrong new one as soon as the list
60
+ // is reordered, filtered, or has an item inserted/removed at the front or
61
+ // middle. The visible result is a class of silent UI bugs that no type check
62
+ // can catch: text typed into one input jumps to another row, the wrong item
63
+ // animates, a checkbox's checked state sticks to the wrong record, and
64
+ // uncontrolled component state generally "smears" across siblings. The fix is
65
+ // to key by a stable, item-specific value (an id, a slug, a content hash),
66
+ // which states the intended identity explicitly. `key={index}` is exactly the
67
+ // shortcut an AI assistant reaches for when the item has no obvious id, so it
68
+ // is squarely in this config's bug-prevention, explicit-over-clever stance —
69
+ // the same correctness-not-style bar that turns on `jsx-no-leaked-render`
70
+ // above while leaving the cosmetic react rules off. Like the other react
71
+ // rules it only applies to `.jsx`/`.tsx` files, so non-React TypeScript
72
+ // packages are unaffected. This is why a downstream repo (`oss-il`) already
73
+ // re-adds it by hand on top of the base config.
74
+ 'react/no-array-index-key': 'error',
75
+ // Forbid the `dangerouslySetInnerHTML` prop. Setting inner HTML directly from
76
+ // a string bypasses React's XSS defenses: any server-sourced or user-supplied
77
+ // content rendered this way can execute arbitrary scripts in the browser.
78
+ // `dangerouslySetInnerHTML` is intentionally verbose — its name is a warning —
79
+ // but AI assistants still reach for it when they need to inject a server HTML
80
+ // fragment or render a rich-text field, without thinking through whether the
81
+ // content is safe. The only legitimate uses (known-safe static markup, a
82
+ // sanitized string from a DOMPurify-style pass) should be reviewed explicitly;
83
+ // the rule forces that review by making every usage a lint error, which the
84
+ // engineer must suppress with an `eslint-disable` comment that makes the
85
+ // intentional decision visible in code review. The rule is not auto-fixable,
86
+ // so each suppression is a deliberate act.
87
+ 'react/no-danger': 'error',
88
+ // Enforce the `[value, setValue]` naming convention for `useState` pairs.
89
+ // When `useState` is destructured with a setter that does not match
90
+ // `set<Capitalize(stateName)>` — e.g. `const [data, handleUpdate] = useState()`
91
+ // instead of `const [data, setData] = useState()` — the mismatch signals a
92
+ // naming mistake that often means the wrong setter was wired up: the right
93
+ // value is read but a misnamed variable is written on update, a class of
94
+ // silent state-management bug that type-checking cannot catch (both names are
95
+ // just `Dispatch<SetStateAction<T>>`). AI assistants introduce exactly this
96
+ // slip when stitching a component from multiple snippets — emitting
97
+ // `const [user, setCurrentUser] = useState()` or
98
+ // `const [loading, handleLoadingChange] = useState(false)`, each of which
99
+ // compiles fine but breaks the per-component naming contract that makes state
100
+ // variables identifiable at a glance. The rule is not auto-fixable because
101
+ // only the author knows whether the state name or the setter name is the
102
+ // intended canonical one. It fires only on `.tsx`/`.jsx` files, so pure
103
+ // TypeScript packages are unaffected.
104
+ 'react/hook-use-state': 'error',
105
+ // Forbid a freshly-constructed value as the `value` of a Context Provider:
106
+ // `<Ctx.Provider value={{ user, setUser }}>` or `value={[state, dispatch]}`
107
+ // or `value={() => ...}`. The object/array/function literal is rebuilt on
108
+ // every render of the providing component, so its identity changes every time
109
+ // even when the data inside is unchanged. React compares context values by
110
+ // reference, so a new reference forces *every* consumer of that context to
111
+ // re-render — including ones that `React.memo`/`useMemo` were added precisely
112
+ // to protect. The result is a silent, whole-subtree performance bug: the app
113
+ // is correct but does far more work than it should, and the cause (an inline
114
+ // literal three components up) is invisible at the consumer. The fix states
115
+ // the intent explicitly — hoist the value into a `useMemo`/`useCallback` (or a
116
+ // stable ref) so the reference only changes when the data does. An inline
117
+ // `value={{ ... }}` is exactly the shortcut an AI assistant emits when wiring
118
+ // up a provider, so it sits on the same correctness/perf-not-style bar that
119
+ // turns on `jsx-no-leaked-render` and `no-array-index-key` above while leaving
120
+ // the cosmetic react rules off. Like the other react rules it only applies to
121
+ // `.jsx`/`.tsx` files, so non-React TypeScript packages are unaffected. This
122
+ // is why a downstream repo (`oss-il`) already re-adds it by hand on top of the
123
+ // base config.
124
+ 'react/jsx-no-constructed-context-values': 'error',
125
+ // Forbid using an object type (`{}`, `[]`, or a function literal) as the
126
+ // default value for a prop in function-component destructuring:
127
+ //
128
+ // function List({ items = [], onSelect = () => {} }) { ... }
129
+ //
130
+ // Each call to the component creates a fresh object/array/function literal
131
+ // for the default, so the reference changes on every render even when the
132
+ // prop is not actually provided. Any child that is `React.memo`-ed on that
133
+ // prop, or any `useEffect`/`useCallback`/`useMemo` that lists it as a
134
+ // dependency, will re-run unnecessarily — a silent performance bug the type
135
+ // checker cannot detect. The fix is to hoist the constant outside the
136
+ // component or memoize it:
137
+ //
138
+ // const DEFAULT_ITEMS: string[] = []
139
+ // function List({ items = DEFAULT_ITEMS }) { ... }
140
+ //
141
+ // This is exactly the shortcut an AI assistant reaches for when a prop is
142
+ // optional and the model doesn't want to add a module-level constant. The
143
+ // rule sits on the same correctness/perf-not-style bar as `jsx-no-leaked-
144
+ // render`, `no-array-index-key`, and `jsx-no-constructed-context-values`
145
+ // above — all of which catch silent reference-equality bugs that are
146
+ // invisible to the type checker. Like the other react rules this only fires
147
+ // on `.jsx`/`.tsx` files, so non-React TypeScript packages are unaffected.
148
+ 'react/no-object-type-as-default-prop': 'error',
149
+ // Require a `key` prop on every element created inside an array/iterator
150
+ // (`.map()`, a spread of JSX literals, etc.). `key` is React's per-child
151
+ // identity across renders; without it React falls back to matching children
152
+ // by position, so as soon as the list is reordered, filtered, or gains/loses
153
+ // an item, state and DOM nodes "smear" onto the wrong element — text typed
154
+ // into one row's input jumps to another, the wrong checkbox stays checked,
155
+ // an animation plays on the wrong item. The JSX is perfectly typed either
156
+ // way, so this is a real rendering bug invisible to the type checker, and
157
+ // exactly the shortcut an AI assistant reaches for when it renders a list
158
+ // without pausing to think about identity. It sits on the same
159
+ // correctness-not-style bar as `no-array-index-key`, `jsx-no-leaked-render`,
160
+ // and `jsx-no-constructed-context-values` above. Like the other react rules
161
+ // it only applies to `.jsx`/`.tsx` files, so non-React TypeScript packages
162
+ // are unaffected.
163
+ 'react/jsx-key': 'error',
16
164
  }