eslint-config-agent 1.3.5 → 1.3.7

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. See [Conven
4
4
 
5
5
 
6
6
 
7
+ ## [1.3.7](https://github.com/tupe12334/eslint-config/compare/v1.3.6...v1.3.7) (2025-09-24)
8
+
9
+ ### Features
10
+
11
+ * add error handling rules from eslint-plugin-error ([082701f](https://github.com/tupe12334/eslint-config/commit/082701f8abe8730ed12afe206caf8f10825b4d1c))
12
+ * add jsx-classname-required rule to enforce className attribute on HTML elements in JSX ([3603865](https://github.com/tupe12334/eslint-config/commit/360386582f05e63d6ba1625548725b9216c4f4d6))
13
+ * integrate error handling rules from eslint-plugin-error and update configuration ([7c8b846](https://github.com/tupe12334/eslint-config/commit/7c8b84694550b92ac2a265591fac736d5ddb1180))
14
+
15
+ ## [1.3.6](https://github.com/tupe12334/eslint-config/compare/v1.3.5...v1.3.6) (2025-09-21)
16
+
17
+ ### Bug Fixes
18
+
19
+ * update TypeScript ESLint integration and remove deprecated parser references ([79ef677](https://github.com/tupe12334/eslint-config/commit/79ef677335cbeb7ed4550da40a0789975dcc68d7))
20
+
7
21
  ## [1.3.5](https://github.com/tupe12334/eslint-config/compare/v1.3.4...v1.3.5) (2025-09-20)
8
22
 
9
23
  ### Bug Fixes
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import js from "@eslint/js";
2
- import tsPlugin from "@typescript-eslint/eslint-plugin";
3
- import tsParser from "@typescript-eslint/parser";
2
+ import tseslint from "typescript-eslint";
4
3
  import reactHooks from "eslint-plugin-react-hooks";
5
4
  import reactPlugin from "eslint-plugin-react";
6
5
  import importPlugin from "eslint-plugin-import";
@@ -13,12 +12,13 @@ import storybookPlugin from "eslint-plugin-storybook";
13
12
  import globals from "globals";
14
13
  import allRules from "./rules/index.js";
15
14
  import { noDefaultClassExportRule } from "./rules/no-default-class-export/index.js";
15
+ import errorPlugin from "eslint-plugin-error";
16
16
 
17
17
  // Conditionally import preact plugin if available
18
18
  let preactPlugin = null;
19
19
  try {
20
20
  preactPlugin = (await import("eslint-plugin-preact")).default;
21
- } catch (error) {
21
+ } catch {
22
22
  // eslint-plugin-preact is not available
23
23
  }
24
24
 
@@ -31,6 +31,7 @@ const sharedRules = {
31
31
  "function-paren-newline": "off",
32
32
  quotes: "off",
33
33
  "no-unused-vars": "off",
34
+ "@typescript-eslint/no-unused-vars": "off",
34
35
  "max-lines-per-function": allRules.maxFunctionLinesWarning,
35
36
  "max-lines": allRules.maxFileLinesWarning,
36
37
  semi: "off",
@@ -40,6 +41,8 @@ const sharedRules = {
40
41
  "implicit-arrow-linebreak": "off",
41
42
  "arrow-body-style": "off",
42
43
  "no-continue": "off",
44
+ // Additional built-in error handling rules
45
+ "prefer-promise-reject-errors": "error",
43
46
  };
44
47
 
45
48
  // Shared no-restricted-syntax rules for both JS and TS
@@ -101,7 +104,6 @@ const sharedRestrictedSyntax = [
101
104
  ...allRules.noDefaultClassExportRules,
102
105
  ];
103
106
 
104
-
105
107
  // TypeScript-specific no-restricted-syntax rules
106
108
  const tsOnlyRestrictedSyntax = [
107
109
  {
@@ -189,9 +191,11 @@ const config = [
189
191
  "class-export": classExportPlugin,
190
192
  "single-export": singleExportPlugin,
191
193
  "required-exports": requiredExportsPlugin,
194
+ error: errorPlugin,
192
195
  custom: {
193
196
  rules: {
194
197
  "no-default-class-export": noDefaultClassExportRule,
198
+ "jsx-classname-required": allRules.jsxClassNameRequiredRule,
195
199
  },
196
200
  },
197
201
  },
@@ -199,18 +203,29 @@ const config = [
199
203
  // Use recommended-latest if available (v5+), otherwise create flat config equivalent of legacy recommended
200
204
  ...(reactHooks.configs["recommended-latest"]
201
205
  ? [reactHooks.configs["recommended-latest"]]
202
- : [{
203
- name: "react-hooks/recommended-flat",
204
- plugins: {
205
- "react-hooks": reactHooks,
206
- },
207
- rules: {
208
- "react-hooks/rules-of-hooks": "error",
209
- "react-hooks/exhaustive-deps": "warn",
206
+ : [
207
+ {
208
+ name: "react-hooks/recommended-flat",
209
+ plugins: {
210
+ "react-hooks": reactHooks,
211
+ },
212
+ rules: {
213
+ "react-hooks/rules-of-hooks": "error",
214
+ "react-hooks/exhaustive-deps": "warn",
215
+ },
210
216
  },
211
- }]
212
- ),
217
+ ]),
213
218
  js.configs.recommended,
219
+ ...tseslint.configs.recommended,
220
+ // Error handling plugin strict config
221
+ {
222
+ plugins: {
223
+ error: errorPlugin,
224
+ },
225
+ rules: {
226
+ ...errorPlugin.configs.strict.rules,
227
+ },
228
+ },
214
229
 
215
230
  // TypeScript and TSX files
216
231
  {
@@ -223,21 +238,12 @@ const config = [
223
238
  "**/*.stories.{js,jsx,ts,tsx}",
224
239
  ],
225
240
  languageOptions: {
226
- parser: tsParser,
227
- parserOptions: {
228
- ecmaVersion: "latest",
229
- sourceType: "module",
230
- ecmaFeatures: {
231
- jsx: true,
232
- },
233
- },
234
241
  globals: {
235
242
  ...globals.browser,
236
243
  ...globals.es2021,
237
244
  },
238
245
  },
239
246
  plugins: {
240
- "@typescript-eslint": tsPlugin,
241
247
  react: reactPlugin,
242
248
  import: importPlugin,
243
249
  security: securityPlugin,
@@ -256,15 +262,18 @@ const config = [
256
262
  "no-undef": "off", // TypeScript handles this
257
263
  "custom/no-default-class-export": "error",
258
264
  "single-export/single-export": "error",
259
- "required-exports/required-exports": ["error", {
260
- "variable": false, // Don't require exporting variables/constants
261
- "function": false, // Don't require exporting functions
262
- "class": true, // Require exporting classes (matches old behavior)
263
- "interface": false, // Don't require exporting interfaces
264
- "type": false, // Don't require exporting types
265
- "enum": true, // Require exporting enums (matches old behavior)
266
- "ignorePrivate": true // Ignore declarations starting with _
267
- }],
265
+ "required-exports/required-exports": [
266
+ "error",
267
+ {
268
+ variable: false, // Don't require exporting variables/constants
269
+ function: false, // Don't require exporting functions
270
+ class: true, // Require exporting classes (matches old behavior)
271
+ interface: false, // Don't require exporting interfaces
272
+ type: false, // Don't require exporting types
273
+ enum: true, // Require exporting enums (matches old behavior)
274
+ ignorePrivate: true, // Ignore declarations starting with _
275
+ },
276
+ ],
268
277
  "no-restricted-syntax": [
269
278
  "error",
270
279
  ...sharedRestrictedSyntax,
@@ -281,15 +290,18 @@ const config = [
281
290
  // Include all shared rules (like max-lines-per-function)
282
291
  ...sharedRules,
283
292
  "single-export/single-export": "off", // TSX files have their own specific export rules
284
- "required-exports/required-exports": ["error", {
285
- "variable": false, // Don't require exporting variables/constants
286
- "function": false, // Don't require exporting functions
287
- "class": true, // Require exporting classes (matches old behavior)
288
- "interface": false, // Don't require exporting interfaces
289
- "type": false, // Don't require exporting types
290
- "enum": true, // Require exporting enums (matches old behavior)
291
- "ignorePrivate": true // Ignore declarations starting with _
292
- }],
293
+ "required-exports/required-exports": [
294
+ "error",
295
+ {
296
+ variable: false, // Don't require exporting variables/constants
297
+ function: false, // Don't require exporting functions
298
+ class: true, // Require exporting classes (matches old behavior)
299
+ interface: false, // Don't require exporting interfaces
300
+ type: false, // Don't require exporting types
301
+ enum: true, // Require exporting enums (matches old behavior)
302
+ ignorePrivate: true, // Ignore declarations starting with _
303
+ },
304
+ ],
293
305
  "no-restricted-syntax": [
294
306
  "error",
295
307
  // Switch case rules as errors
@@ -351,12 +363,6 @@ const config = [
351
363
  message:
352
364
  "Function expressions containing switch statements must have explicit return type annotations.",
353
365
  },
354
- // className requirement for HTML elements
355
- {
356
- selector:
357
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
358
- message: "HTML elements must have a className attribute.",
359
- },
360
366
  ],
361
367
  },
362
368
  },
@@ -482,19 +488,19 @@ const config = [
482
488
  rules: {
483
489
  ...sharedRules,
484
490
  "single-export/single-export": "error",
485
- "required-exports/required-exports": ["error", {
486
- "variable": false, // Don't require exporting variables/constants
487
- "function": false, // Don't require exporting functions
488
- "class": true, // Require exporting classes (matches old behavior)
489
- "interface": false, // Don't require exporting interfaces (N/A for JS)
490
- "type": false, // Don't require exporting types (N/A for JS)
491
- "enum": false, // Don't require exporting enums (N/A for JS)
492
- "ignorePrivate": true // Ignore declarations starting with _
493
- }],
494
- "no-restricted-syntax": [
491
+ "required-exports/required-exports": [
495
492
  "error",
496
- ...sharedRestrictedSyntax,
493
+ {
494
+ variable: false, // Don't require exporting variables/constants
495
+ function: false, // Don't require exporting functions
496
+ class: true, // Require exporting classes (matches old behavior)
497
+ interface: false, // Don't require exporting interfaces (N/A for JS)
498
+ type: false, // Don't require exporting types (N/A for JS)
499
+ enum: false, // Don't require exporting enums (N/A for JS)
500
+ ignorePrivate: true, // Ignore declarations starting with _
501
+ },
497
502
  ],
503
+ "no-restricted-syntax": ["error", ...sharedRestrictedSyntax],
498
504
  },
499
505
  },
500
506
 
@@ -523,21 +529,12 @@ const config = [
523
529
  "**/*.stories.{js,jsx,ts,tsx}",
524
530
  ],
525
531
  languageOptions: {
526
- parser: tsParser,
527
- parserOptions: {
528
- ecmaVersion: "latest",
529
- sourceType: "module",
530
- ecmaFeatures: {
531
- jsx: true,
532
- },
533
- },
534
532
  globals: {
535
533
  ...globals.browser,
536
534
  ...globals.es2021,
537
535
  },
538
536
  },
539
537
  plugins: {
540
- "@typescript-eslint": tsPlugin,
541
538
  react: reactPlugin,
542
539
  import: importPlugin,
543
540
  security: securityPlugin,
@@ -555,15 +552,18 @@ const config = [
555
552
  ...sharedRules,
556
553
  "no-undef": "off", // TypeScript handles this
557
554
  "single-export/single-export": "off", // JSX files have their own specific export rules
558
- "required-exports/required-exports": ["error", {
559
- "variable": false, // Don't require exporting variables/constants
560
- "function": false, // Don't require exporting functions
561
- "class": true, // Require exporting classes (matches old behavior)
562
- "interface": false, // Don't require exporting interfaces (N/A for JSX)
563
- "type": false, // Don't require exporting types (N/A for JSX)
564
- "enum": false, // Don't require exporting enums (N/A for JSX)
565
- "ignorePrivate": true // Ignore declarations starting with _
566
- }],
555
+ "required-exports/required-exports": [
556
+ "error",
557
+ {
558
+ variable: false, // Don't require exporting variables/constants
559
+ function: false, // Don't require exporting functions
560
+ class: true, // Require exporting classes (matches old behavior)
561
+ interface: false, // Don't require exporting interfaces (N/A for JSX)
562
+ type: false, // Don't require exporting types (N/A for JSX)
563
+ enum: false, // Don't require exporting enums (N/A for JSX)
564
+ ignorePrivate: true, // Ignore declarations starting with _
565
+ },
566
+ ],
567
567
  "no-restricted-syntax": [
568
568
  "error",
569
569
  // Switch case rules as errors
@@ -625,12 +625,6 @@ const config = [
625
625
  message:
626
626
  "Function expressions containing switch statements must have explicit return type annotations.",
627
627
  },
628
- // className requirement for HTML elements
629
- {
630
- selector:
631
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
632
- message: "HTML elements must have a className attribute.",
633
- },
634
628
  // Required export rules as errors (class rule only for JSX)
635
629
  {
636
630
  selector:
@@ -653,21 +647,12 @@ const config = [
653
647
  "**/*.stories.{js,jsx,ts,tsx}",
654
648
  ],
655
649
  languageOptions: {
656
- parser: tsParser,
657
- parserOptions: {
658
- ecmaVersion: "latest",
659
- sourceType: "module",
660
- ecmaFeatures: {
661
- jsx: true,
662
- },
663
- },
664
650
  globals: {
665
651
  ...globals.browser,
666
652
  ...globals.es2021,
667
653
  },
668
654
  },
669
655
  plugins: {
670
- "@typescript-eslint": tsPlugin,
671
656
  react: reactPlugin,
672
657
  import: importPlugin,
673
658
  security: securityPlugin,
@@ -684,15 +669,18 @@ const config = [
684
669
  rules: {
685
670
  "no-undef": "off", // TypeScript handles this
686
671
  "single-export/single-export": "off", // JSX files have their own specific export rules
687
- "required-exports/required-exports": ["warn", {
688
- "variable": false, // Don't require exporting variables/constants
689
- "function": false, // Don't require exporting functions
690
- "class": true, // Require exporting classes (matches old behavior)
691
- "interface": false, // Don't require exporting interfaces (N/A for JSX)
692
- "type": false, // Don't require exporting types (N/A for JSX)
693
- "enum": false, // Don't require exporting enums (N/A for JSX)
694
- "ignorePrivate": true // Ignore declarations starting with _
695
- }],
672
+ "required-exports/required-exports": [
673
+ "warn",
674
+ {
675
+ variable: false, // Don't require exporting variables/constants
676
+ function: false, // Don't require exporting functions
677
+ class: true, // Require exporting classes (matches old behavior)
678
+ interface: false, // Don't require exporting interfaces (N/A for JSX)
679
+ type: false, // Don't require exporting types (N/A for JSX)
680
+ enum: false, // Don't require exporting enums (N/A for JSX)
681
+ ignorePrivate: true, // Ignore declarations starting with _
682
+ },
683
+ ],
696
684
  "no-restricted-syntax": [
697
685
  "warn",
698
686
  // Include shared rules but remove the multiple exports restriction and switch case rules for JSX
@@ -855,18 +843,22 @@ const config = [
855
843
  "**/test/**",
856
844
  "!**/test/export/**",
857
845
  "!**/test/required-exports/**",
846
+ "!**/test/switch-case/**",
858
847
  "**/rules/**/index.js",
859
848
  ],
860
849
  rules: {
861
- "required-exports/required-exports": ["error", {
862
- "variable": false, // Don't require exporting variables/constants
863
- "function": false, // Don't require exporting functions
864
- "class": true, // Require exporting classes (matches old behavior)
865
- "interface": false, // Don't require exporting interfaces
866
- "type": false, // Don't require exporting types
867
- "enum": true, // Require exporting enums (matches old behavior)
868
- "ignorePrivate": true // Ignore declarations starting with _
869
- }],
850
+ "required-exports/required-exports": [
851
+ "error",
852
+ {
853
+ variable: false, // Don't require exporting variables/constants
854
+ function: false, // Don't require exporting functions
855
+ class: true, // Require exporting classes (matches old behavior)
856
+ interface: false, // Don't require exporting interfaces
857
+ type: false, // Don't require exporting types
858
+ enum: true, // Require exporting enums (matches old behavior)
859
+ ignorePrivate: true, // Ignore declarations starting with _
860
+ },
861
+ ],
870
862
  "no-restricted-syntax": [
871
863
  "error",
872
864
  // Switch case rules as errors
@@ -959,14 +951,7 @@ const config = [
959
951
  files: ["**/*.{tsx,jsx}"],
960
952
  ignores: ["**/*.stories.{js,jsx,ts,tsx}"],
961
953
  rules: {
962
- "no-restricted-syntax": [
963
- "error",
964
- {
965
- selector:
966
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
967
- message: "HTML elements must have a className attribute.",
968
- },
969
- ],
954
+ "custom/jsx-classname-required": "error",
970
955
  },
971
956
  },
972
957
 
@@ -1019,14 +1004,6 @@ const config = [
1019
1004
  {
1020
1005
  files: ["**/*.stories.{js,jsx,ts,tsx}"],
1021
1006
  languageOptions: {
1022
- parser: tsParser,
1023
- parserOptions: {
1024
- ecmaVersion: "latest",
1025
- sourceType: "module",
1026
- ecmaFeatures: {
1027
- jsx: true,
1028
- },
1029
- },
1030
1007
  globals: {
1031
1008
  ...globals.browser,
1032
1009
  ...globals.es2021,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-config-agent",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "ESLint configuration package with TypeScript support",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -106,5 +106,8 @@
106
106
  "globals": "^16.3.0",
107
107
  "release-it": "^19.0.4",
108
108
  "typescript-eslint": "^8.40.0"
109
+ },
110
+ "dependencies": {
111
+ "eslint-plugin-error": "^1.1.1"
109
112
  }
110
113
  }
package/rules/index.js CHANGED
@@ -16,6 +16,7 @@ import { noTypeAssertionsConfig } from "./no-type-assertions/index.js";
16
16
  import { noExportSpecifiersConfig } from "./no-empty-exports/index.js";
17
17
  import { noClassPropertyDefaultsConfig } from "./no-class-property-defaults/index.js";
18
18
  import { noDefaultClassExportRules } from "./no-default-class-export/index.js";
19
+ import { jsxClassNameRequiredRule } from "./jsx-classname-required/index.js";
19
20
 
20
21
  // Plugin rule configurations
21
22
  import { pluginRules } from "./plugin/index.js";
@@ -36,6 +37,7 @@ const allRules = {
36
37
  noExportSpecifiersConfig,
37
38
  noClassPropertyDefaultsConfig,
38
39
  noDefaultClassExportRules,
40
+ jsxClassNameRequiredRule,
39
41
 
40
42
  // Plugin rule configurations
41
43
  pluginRules,
@@ -59,6 +61,7 @@ export {
59
61
  noExportSpecifiersConfig,
60
62
  noClassPropertyDefaultsConfig,
61
63
  noDefaultClassExportRules,
64
+ jsxClassNameRequiredRule,
62
65
 
63
66
  // Plugin rule configurations
64
67
  pluginRules,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ESLint rule to require className attribute on HTML elements in JSX
3
+ *
4
+ * This rule enforces that all HTML elements in JSX must have a className attribute.
5
+ * It excludes React components (starting with capital letters) and fragments.
6
+ *
7
+ * Examples:
8
+ * - ❌ <div>Content</div>
9
+ * - ✅ <div className="container">Content</div>
10
+ * - ✅ <MyComponent>Content</MyComponent> (ignored - React component)
11
+ * - ✅ <Fragment>Content</Fragment> (ignored - fragment)
12
+ * - ✅ <React.Fragment>Content</React.Fragment> (ignored - React fragment)
13
+ * - ✅ <React.StrictMode>Content</React.StrictMode> (ignored - React component)
14
+ */
15
+
16
+ const jsxClassNameRequiredRule = {
17
+ meta: {
18
+ type: "layout",
19
+ docs: {
20
+ description: "Require className attribute on HTML elements in JSX",
21
+ category: "Stylistic Issues",
22
+ recommended: false,
23
+ },
24
+ fixable: null,
25
+ schema: [],
26
+ messages: {
27
+ missingClassName: "HTML elements must have a className attribute.",
28
+ },
29
+ },
30
+
31
+ create(context) {
32
+ return {
33
+ JSXOpeningElement(node) {
34
+ // Skip if this is a React component (starts with capital letter)
35
+ if (node.name.type === 'JSXIdentifier' && /^[A-Z]/.test(node.name.name)) {
36
+ return;
37
+ }
38
+
39
+ // Skip if this is Fragment
40
+ if (node.name.type === 'JSXIdentifier' && node.name.name === 'Fragment') {
41
+ return;
42
+ }
43
+
44
+ // Skip if this is React.Something (like React.Fragment, React.StrictMode, etc.)
45
+ if (node.name.type === 'JSXMemberExpression' &&
46
+ node.name.object.name === 'React' &&
47
+ /^[A-Z]/.test(node.name.property.name)) {
48
+ return;
49
+ }
50
+
51
+ // Check if element has className attribute
52
+ const hasClassName = node.attributes.some(attr =>
53
+ attr.type === 'JSXAttribute' &&
54
+ attr.name.name === 'className'
55
+ );
56
+
57
+ if (!hasClassName) {
58
+ context.report({
59
+ node,
60
+ messageId: 'missingClassName',
61
+ });
62
+ }
63
+ },
64
+ };
65
+ },
66
+ };
67
+
68
+ export { jsxClassNameRequiredRule };
@@ -0,0 +1,283 @@
1
+ import { RuleTester } from "eslint";
2
+ import { jsxClassNameRequiredRule } from "./index.js";
3
+
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: "module",
8
+ parser: (await import("typescript-eslint")).parser,
9
+ parserOptions: {
10
+ ecmaFeatures: {
11
+ jsx: true,
12
+ },
13
+ },
14
+ },
15
+ });
16
+
17
+ ruleTester.run('jsx-classname-required', jsxClassNameRequiredRule, {
18
+ valid: [
19
+ // HTML elements with className
20
+ {
21
+ code: '<div className="container">Content</div>',
22
+ },
23
+ {
24
+ code: '<span className="text">Text</span>',
25
+ },
26
+ {
27
+ code: '<p className="paragraph">Paragraph</p>',
28
+ },
29
+ {
30
+ code: '<button className="btn">Button</button>',
31
+ },
32
+ {
33
+ code: '<input className="input" type="text" />',
34
+ },
35
+ {
36
+ code: '<img className="image" src="test.jpg" alt="test" />',
37
+ },
38
+
39
+ // React components (should be ignored)
40
+ {
41
+ code: '<MyComponent>Content</MyComponent>',
42
+ },
43
+ {
44
+ code: '<CustomButton onClick={() => {}}>Click</CustomButton>',
45
+ },
46
+ {
47
+ code: '<AnotherComponent prop="value" />',
48
+ },
49
+
50
+ // Fragments (should be ignored)
51
+ {
52
+ code: '<Fragment>Content</Fragment>',
53
+ },
54
+ {
55
+ code: '<React.Fragment>Content</React.Fragment>',
56
+ },
57
+ {
58
+ code: '<>Content</>',
59
+ },
60
+
61
+ // React.* components (should be ignored)
62
+ {
63
+ code: '<React.StrictMode><div className="content">Content</div></React.StrictMode>',
64
+ },
65
+ {
66
+ code: '<React.Suspense fallback={<div className="loading">Loading</div>}><div className="content">Content</div></React.Suspense>',
67
+ },
68
+ {
69
+ code: '<React.Profiler id="test" onRender={() => {}}><div className="content">Content</div></React.Profiler>',
70
+ },
71
+
72
+ // Mixed cases with valid HTML elements
73
+ {
74
+ code: `
75
+ <div className="container">
76
+ <MyComponent>
77
+ <p className="text">Text</p>
78
+ </MyComponent>
79
+ <React.Fragment>
80
+ <span className="fragment-content">Fragment content</span>
81
+ </React.Fragment>
82
+ </div>
83
+ `,
84
+ },
85
+
86
+ // Nested valid cases
87
+ {
88
+ code: `
89
+ <div className="outer">
90
+ <div className="inner">
91
+ <span className="nested">Nested content</span>
92
+ </div>
93
+ </div>
94
+ `,
95
+ },
96
+ ],
97
+
98
+ invalid: [
99
+ // HTML elements without className
100
+ {
101
+ code: '<div>Content</div>',
102
+ errors: [
103
+ {
104
+ messageId: 'missingClassName',
105
+ type: 'JSXOpeningElement',
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ code: '<span>Text</span>',
111
+ errors: [
112
+ {
113
+ messageId: 'missingClassName',
114
+ type: 'JSXOpeningElement',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ code: '<p>Paragraph</p>',
120
+ errors: [
121
+ {
122
+ messageId: 'missingClassName',
123
+ type: 'JSXOpeningElement',
124
+ },
125
+ ],
126
+ },
127
+ {
128
+ code: '<button>Button</button>',
129
+ errors: [
130
+ {
131
+ messageId: 'missingClassName',
132
+ type: 'JSXOpeningElement',
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ code: '<input type="text" />',
138
+ errors: [
139
+ {
140
+ messageId: 'missingClassName',
141
+ type: 'JSXOpeningElement',
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ code: '<img src="test.jpg" alt="test" />',
147
+ errors: [
148
+ {
149
+ messageId: 'missingClassName',
150
+ type: 'JSXOpeningElement',
151
+ },
152
+ ],
153
+ },
154
+
155
+ // HTML elements inside React components (should still error)
156
+ {
157
+ code: '<MyComponent><div>Content</div></MyComponent>',
158
+ errors: [
159
+ {
160
+ messageId: 'missingClassName',
161
+ type: 'JSXOpeningElement',
162
+ },
163
+ ],
164
+ },
165
+ {
166
+ code: '<React.StrictMode><div>Content</div></React.StrictMode>',
167
+ errors: [
168
+ {
169
+ messageId: 'missingClassName',
170
+ type: 'JSXOpeningElement',
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ code: '<React.Fragment><span>Content</span></React.Fragment>',
176
+ errors: [
177
+ {
178
+ messageId: 'missingClassName',
179
+ type: 'JSXOpeningElement',
180
+ },
181
+ ],
182
+ },
183
+ {
184
+ code: '<Fragment><p>Content</p></Fragment>',
185
+ errors: [
186
+ {
187
+ messageId: 'missingClassName',
188
+ type: 'JSXOpeningElement',
189
+ },
190
+ ],
191
+ },
192
+ {
193
+ code: '<><div>Content</div></>',
194
+ errors: [
195
+ {
196
+ messageId: 'missingClassName',
197
+ type: 'JSXOpeningElement',
198
+ },
199
+ ],
200
+ },
201
+
202
+ // Multiple errors in one file
203
+ {
204
+ code: `
205
+ <div className="container">
206
+ <span>Missing className</span>
207
+ <p>Also missing className</p>
208
+ </div>
209
+ `,
210
+ errors: [
211
+ {
212
+ messageId: 'missingClassName',
213
+ type: 'JSXOpeningElement',
214
+ },
215
+ {
216
+ messageId: 'missingClassName',
217
+ type: 'JSXOpeningElement',
218
+ },
219
+ ],
220
+ },
221
+
222
+ // Mixed valid/invalid cases
223
+ {
224
+ code: `
225
+ <div className="container">
226
+ <span className="valid">Valid</span>
227
+ <p>Invalid - no className</p>
228
+ <MyComponent>
229
+ <div>Invalid - no className</div>
230
+ </MyComponent>
231
+ </div>
232
+ `,
233
+ errors: [
234
+ {
235
+ messageId: 'missingClassName',
236
+ type: 'JSXOpeningElement',
237
+ },
238
+ {
239
+ messageId: 'missingClassName',
240
+ type: 'JSXOpeningElement',
241
+ },
242
+ ],
243
+ },
244
+
245
+ // Edge cases
246
+ {
247
+ code: '<h1>Heading</h1>',
248
+ errors: [
249
+ {
250
+ messageId: 'missingClassName',
251
+ type: 'JSXOpeningElement',
252
+ },
253
+ ],
254
+ },
255
+ {
256
+ code: '<section>Section</section>',
257
+ errors: [
258
+ {
259
+ messageId: 'missingClassName',
260
+ type: 'JSXOpeningElement',
261
+ },
262
+ ],
263
+ },
264
+ {
265
+ code: '<article>Article</article>',
266
+ errors: [
267
+ {
268
+ messageId: 'missingClassName',
269
+ type: 'JSXOpeningElement',
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ code: '<nav>Navigation</nav>',
275
+ errors: [
276
+ {
277
+ messageId: 'missingClassName',
278
+ type: 'JSXOpeningElement',
279
+ },
280
+ ],
281
+ },
282
+ ],
283
+ });