diff --git a/package-lock.json b/package-lock.json
index 28a95bbf..62bd9cfa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4166,6 +4166,25 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -5141,6 +5160,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -9240,6 +9274,18 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
"packages/code-analyzer-core": {
"name": "@salesforce/code-analyzer-core",
"version": "0.41.0",
@@ -9770,6 +9816,7 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.5.0",
"eslint-plugin-react": "^7.37.2",
+ "eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
"semver": "^7.7.3",
"typescript": "^5.9.3",
diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json
index 3c4d0e36..147c903f 100644
--- a/packages/code-analyzer-eslint-engine/package.json
+++ b/packages/code-analyzer-eslint-engine/package.json
@@ -29,6 +29,7 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.5.0",
"eslint-plugin-react": "^7.37.2",
+ "eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
"semver": "^7.7.3",
"typescript": "^5.9.3",
diff --git a/packages/code-analyzer-eslint-engine/src/base-config.ts b/packages/code-analyzer-eslint-engine/src/base-config.ts
index 88425804..4cb58a84 100644
--- a/packages/code-analyzer-eslint-engine/src/base-config.ts
+++ b/packages/code-analyzer-eslint-engine/src/base-config.ts
@@ -5,6 +5,7 @@ import lwcEslintPluginLwcPlatform from "@lwc/eslint-plugin-lwc-platform";
import salesforceEslintConfigLwc from "@salesforce/eslint-config-lwc";
import sldsEslintPlugin from "@salesforce-ux/eslint-plugin-slds";
import eslintPluginReact from "eslint-plugin-react";
+import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import {ESLintEngineConfig} from "./config";
import globals from "globals";
@@ -63,7 +64,7 @@ export class BaseConfigFactory {
}
private createJavascriptPlusLwcConfigArray(): Linter.Config[] {
- let configs: Linter.Config[] = validateAndGetRawLwcConfigArray();
+ const configs: Linter.Config[] = validateAndGetRawLwcConfigArray();
// Reconstruct languageOptions to avoid mutating the original shared config from the LWC package
// TODO: Remove configFile and sourceType overrides when https://github.com/salesforce/eslint-config-lwc/issues/158 is fixed
@@ -88,8 +89,17 @@ export class BaseConfigFactory {
}
};
- // Swap out eslintJs.configs.recommended with eslintJs.configs.all
- configs[1] = eslintJs.configs.all;
+ // File patterns for different config types
+ const allJsExtensions = this.engineConfig.file_extensions.javascript;
+ const lwcExtensions = allJsExtensions.filter(ext => ext !== '.jsx');
+ const allJsFilePatterns = allJsExtensions.map(ext => `**/*${ext}`);
+ const lwcFilePatterns = lwcExtensions.map(ext => `**/*${ext}`);
+
+ // Base JS rules (eslintJs.configs.all) - applies to ALL JS files including .jsx
+ const baseJsConfig: Linter.Config = {
+ ...eslintJs.configs.all,
+ files: allJsFilePatterns
+ };
// This one rule makes eslint throw an exception if the user doesn't have jest installed (which should be
// optional), so we turn it off for now. See https://github.com/salesforce/eslint-config-lwc/issues/161
@@ -105,18 +115,15 @@ export class BaseConfigFactory {
'@lwc/lwc-platform/valid-offline-wire': 'off'
}
- // Restrict these configs to just javascript files, excluding .jsx
- // since those are React files, not LWC components
- const lwcExtensions = this.engineConfig.file_extensions.javascript
- .filter(ext => ext !== '.jsx');
- configs = configs.map(config => {
- return {
- ...config,
- files: lwcExtensions.map(ext => `**/*${ext}`)
- }
- });
+ // Apply LWC file patterns to LWC-specific configs (excludes .jsx - React files aren't LWC)
+ // Then insert the base JS config at position 1
+ const lwcConfigs: Linter.Config[] = configs.map(config => ({
+ ...config,
+ files: lwcFilePatterns
+ }));
+ lwcConfigs[1] = baseJsConfig;
- return configs;
+ return lwcConfigs;
}
private createLwcConfigArray(): Linter.Config[] {
@@ -191,16 +198,17 @@ export class BaseConfigFactory {
}
/**
- * Creates React plugin config for JavaScript files.
+ * Creates React plugin config for JavaScript and TypeScript files.
*
- * React rules are applied to all JS files (.js, .jsx, .cjs, .mjs) - if a file
- * doesn't contain React code, the rules simply won't report any violations.
+ * Includes both eslint-plugin-react and eslint-plugin-react-hooks:
+ * - react/*: All React rules for JSX/TSX and component patterns
+ * - react-hooks/rules-of-hooks: Enforces the Rules of Hooks
+ * - react-hooks/exhaustive-deps: Verifies the list of dependencies for Hooks
*
- * Note: TypeScript React support (.tsx) is planned for the next iteration.
+ * React rules are applied to all JS/TS files - if a file doesn't contain React code,
+ * the rules simply won't report any violations.
*/
private createReactConfigArray(): Linter.Config[] {
- // Apply React rules to all JavaScript and TypeScript files
-
const jsExtensions = this.engineConfig.file_extensions.javascript;
const tsExtensions = this.engineConfig.file_extensions.typescript;
const reactExtensions = [...new Set([...jsExtensions, ...tsExtensions])];
@@ -209,22 +217,46 @@ export class BaseConfigFactory {
return [];
}
+ const filePatterns = reactExtensions.map(ext => `**/*${ext}`);
+
// Get all rules from eslint-plugin-react's flat config
const reactAllConfig = eslintPluginReact.configs.flat.all;
- return [{
- ...reactAllConfig,
- files: reactExtensions.map(ext => `**/*${ext}`),
- settings: {
- ...reactAllConfig.settings,
- react: {
- // React version - "detect" automatically picks the installed version, falls back to latest
- version: 'detect',
- // Pragma is the function JSX compiles to (e.g.,
→ React.createElement('div'))
- pragma: 'React'
+ // Get jsx-runtime config to disable outdated rules (react-in-jsx-scope, jsx-uses-react)
+ // These rules are not needed for React 17+ which is now the standard (released Oct 2020)
+ const jsxRuntimeConfig = eslintPluginReact.configs.flat['jsx-runtime'];
+
+ return [
+ // React all rules config
+ {
+ ...reactAllConfig,
+ files: filePatterns,
+ settings: {
+ ...reactAllConfig.settings,
+ react: {
+ // React version - "detect" automatically picks the installed version, falls back to latest
+ version: 'detect',
+ // Pragma is the function JSX compiles to (e.g.,
→ React.createElement('div'))
+ pragma: 'React'
+ }
+ }
+ },
+ // jsx-runtime config disables outdated rules for React 17+
+ {
+ ...jsxRuntimeConfig,
+ files: filePatterns
+ },
+ // React Hooks plugin config - use flat.recommended but only enable the 2 classic rules
+ // (v7.x includes many React Compiler rules that we filter out)
+ {
+ ...eslintPluginReactHooks.configs.flat.recommended,
+ files: filePatterns,
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn'
}
}
- }];
+ ];
}
private useJsBaseConfig(): boolean {
diff --git a/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts b/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts
index 66fdc688..ff880f3c 100644
--- a/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts
+++ b/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts
@@ -60,11 +60,7 @@ declare module 'eslint-plugin-react' {
const plugin: ESLint.Plugin & {
readonly rules: Record;
readonly configs: {
- readonly recommended: Linter.Config;
- readonly all: Linter.Config;
- readonly "jsx-runtime": Linter.Config;
readonly flat: {
- readonly recommended: Linter.Config;
readonly all: Linter.Config;
readonly "jsx-runtime": Linter.Config;
};
@@ -72,3 +68,20 @@ declare module 'eslint-plugin-react' {
};
export = plugin;
}
+
+// This declaration adds in the missing types for "eslint-plugin-react-hooks"
+// We use configs.flat.recommended but override rules to only enable rules-of-hooks and exhaustive-deps
+declare module 'eslint-plugin-react-hooks' {
+ import type { ESLint, Linter } from 'eslint';
+ import type { RuleDefinition } from "@eslint/core";
+
+ const plugin: ESLint.Plugin & {
+ readonly rules: Record;
+ readonly configs: {
+ readonly flat: {
+ readonly recommended: Linter.Config;
+ };
+ };
+ };
+ export = plugin;
+}
diff --git a/packages/code-analyzer-eslint-engine/src/rule-mappings/react.ts b/packages/code-analyzer-eslint-engine/src/rule-mappings/react.ts
index 345a90ff..1e3dc70f 100644
--- a/packages/code-analyzer-eslint-engine/src/rule-mappings/react.ts
+++ b/packages/code-analyzer-eslint-engine/src/rule-mappings/react.ts
@@ -26,11 +26,7 @@ export const RULE_MAPPINGS_REACT_RECOMMENDED: Record = {
+ "react-hooks/rules-of-hooks": {
+ severity: SeverityLevel.High,
+ tags: [COMMON_TAGS.RECOMMENDED, REACT, COMMON_TAGS.CATEGORIES.DESIGN, COMMON_TAGS.LANGUAGES.JAVASCRIPT, COMMON_TAGS.LANGUAGES.TYPESCRIPT]
+ },
+ "react-hooks/exhaustive-deps": {
+ severity: SeverityLevel.Moderate,
+ tags: [COMMON_TAGS.RECOMMENDED, REACT, COMMON_TAGS.CATEGORIES.DESIGN, COMMON_TAGS.LANGUAGES.JAVASCRIPT, COMMON_TAGS.LANGUAGES.TYPESCRIPT]
+ },
+};
+
export const RULE_MAPPINGS_REACT: Record = {
...RULE_MAPPINGS_REACT_RECOMMENDED,
- ...RULE_MAPPINGS_REACT_NOT_RECOMMENDED
+ ...RULE_MAPPINGS_REACT_NOT_RECOMMENDED,
+ ...RULE_MAPPINGS_REACT_HOOKS
};
diff --git a/packages/code-analyzer-eslint-engine/test/engine.test.ts b/packages/code-analyzer-eslint-engine/test/engine.test.ts
index 72960c68..278acc4c 100644
--- a/packages/code-analyzer-eslint-engine/test/engine.test.ts
+++ b/packages/code-analyzer-eslint-engine/test/engine.test.ts
@@ -65,7 +65,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
const HTML_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlySldsHtmlBaseConfig.goldfile.json');
const REACT_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_ReactConfig.goldfile.json');
const SLDS_CONFIG_RULES: RuleDescription[] = makeUniqueAndSorted([...CSS_CONFIG_RULES, ...HTML_CONFIG_RULES]);
- // React rules apply to all JS files (not just .jsx) - if no React code, rules simply don't report violations
+ // React rules (including React Hooks) apply to all JS files - if no React code, rules simply don't report violations
const DEFAULT_RULES: RuleDescription[] = makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...SLDS_CONFIG_RULES, ...REACT_CONFIG_RULES]);
const CUSTOM_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlyCustomConfigWithNewRules.goldfile.json');
@@ -150,7 +150,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions(new Workspace('id', [
path.join(workspaceWithNoCustomConfig, 'dummy1.js'),
path.join(workspaceWithNoCustomConfig, 'dummy3.txt')])));
- // React rules included - applies to .js files
+ // React rules (including React Hooks) included - applies to .js files
expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...REACT_CONFIG_RULES]));
});
@@ -172,7 +172,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
disable_javascript_base_config: true
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions());
- // React rules included - no workspace provided means placeholder files used (includes .jsx)
+ // React rules (including React Hooks) included - no workspace provided means placeholder files used (includes .jsx)
expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...TS_CONFIG_RULES, ...SLDS_CONFIG_RULES, ...REACT_CONFIG_RULES]));
});
@@ -181,7 +181,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
disable_lwc_base_config: true
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions());
- // React rules included - no workspace provided means placeholder files used (includes .jsx)
+ // React rules (including React Hooks) included - no workspace provided means placeholder files used (includes .jsx)
expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...SLDS_CONFIG_RULES, ...REACT_CONFIG_RULES]));
});
@@ -190,7 +190,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
disable_slds_base_config: true
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions());
- // React rules included - no workspace provided means placeholder files used (includes .jsx)
+ // React rules (including React Hooks) included - no workspace provided means placeholder files used (includes .jsx)
expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...REACT_CONFIG_RULES]));
});
@@ -210,7 +210,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
disable_slds_base_config: true
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions());
- // React rules included - no workspace provided means placeholder files used (includes .jsx)
+ // React rules (including React Hooks) included - no workspace provided means placeholder files used (includes .jsx)
expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...JS_CONFIG_RULES, ...REACT_CONFIG_RULES]));
});
@@ -848,6 +848,83 @@ describe('Typical tests for the runRules method of ESLintEngine', () => {
v.codeLocations[0].file.endsWith('.jsx'));
expect(jsxViolations.length).toBeGreaterThan(0);
});
+
+ it('When runRules is called with react-hooks rules, then violations are detected in HooksViolations.jsx', async () => {
+ const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING);
+ const runOptions: RunOptions = createRunOptions(new Workspace('id', [workspaceWithReactFiles]));
+ const results: EngineRunResults = await engine.runRules(['react-hooks/rules-of-hooks', 'react-hooks/exhaustive-deps'], runOptions);
+
+ // HooksViolations.jsx has multiple violations for both rules
+ expect(results.violations.length).toBeGreaterThan(0);
+
+ // Should have violations for rules-of-hooks (hooks called conditionally/in loops)
+ const rulesOfHooksViolations = results.violations.filter(v => v.ruleName === 'react-hooks/rules-of-hooks');
+ expect(rulesOfHooksViolations.length).toBeGreaterThan(0);
+
+ // Should have violations for exhaustive-deps (missing dependencies)
+ const exhaustiveDepsViolations = results.violations.filter(v => v.ruleName === 'react-hooks/exhaustive-deps');
+ expect(exhaustiveDepsViolations.length).toBeGreaterThan(0);
+
+ // All violations should be in HooksViolations.jsx
+ const hooksViolationsFile = results.violations.filter(v =>
+ v.codeLocations[0].file.endsWith('HooksViolations.jsx'));
+ expect(hooksViolationsFile.length).toBeGreaterThan(0);
+ });
+
+ it('When runRules is called with base JS rules on .jsx files, then violations are reported', async () => {
+ // This test verifies that base JavaScript rules (not just React-specific rules) are applied to .jsx files
+ // This is a regression test - previously .jsx files were excluded from base JS rules when both JS+LWC configs were enabled
+ const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING);
+ const runOptions: RunOptions = createRunOptions(new Workspace('id', [workspaceWithReactFiles]));
+
+ // Use 'id-length' - a base JS rule that triggers on short variable names like 'i' in for loops
+ const results: EngineRunResults = await engine.runRules(['id-length'], runOptions);
+
+ // Should have violations in .jsx files (HooksViolations.jsx has 'i' variable in for loop)
+ const jsxViolations = results.violations.filter(v =>
+ v.codeLocations[0].file.endsWith('.jsx'));
+ expect(jsxViolations.length).toBeGreaterThan(0);
+
+ // Verify the rule is a base JS rule, not a React rule
+ expect(jsxViolations.every(v => v.ruleName === 'id-length')).toBe(true);
+ });
+
+ it('When runRules is called with LWC rules, then no violations are reported for .jsx files', async () => {
+ // This test verifies that LWC-specific rules are NOT applied to .jsx files
+ // .jsx files are React files, not LWC components, so LWC rules should not apply
+ // HooksViolations.jsx contains innerHTML usage (ComponentWithInnerHTML) that WOULD
+ // trigger @lwc/lwc/no-inner-html if LWC rules were applied, but they shouldn't be.
+ const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING);
+ const runOptions: RunOptions = createRunOptions(new Workspace('id', [workspaceWithReactFiles]));
+
+ // Run '@lwc/lwc/no-inner-html' - HooksViolations.jsx has innerHTML usage that would trigger this
+ const results: EngineRunResults = await engine.runRules(['@lwc/lwc/no-inner-html'], runOptions);
+
+ // Should NOT have any violations in .jsx files (LWC rules don't apply to React files)
+ const jsxViolations = results.violations.filter(v =>
+ v.codeLocations[0].file.endsWith('.jsx'));
+ expect(jsxViolations.length).toBe(0);
+ });
+});
+
+describe('Tests for React Hooks rules', () => {
+ it('React Hooks rules are included in describeRules output', async () => {
+ const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING);
+
+ const ruleDescriptions = await engine.describeRules(createDescribeOptions(
+ new Workspace('id', [workspaceWithReactFiles])));
+
+ // Both react-hooks rules should be present
+ const rulesOfHooks = ruleDescriptions.find(r => r.name === 'react-hooks/rules-of-hooks');
+ expect(rulesOfHooks).toBeDefined();
+ expect(rulesOfHooks!.tags).toContain('Recommended');
+ expect(rulesOfHooks!.tags).toContain('React');
+
+ const exhaustiveDeps = ruleDescriptions.find(r => r.name === 'react-hooks/exhaustive-deps');
+ expect(exhaustiveDeps).toBeDefined();
+ expect(exhaustiveDeps!.tags).toContain('Recommended');
+ expect(exhaustiveDeps!.tags).toContain('React');
+ });
});
describe('Tests for the getEngineVersion method of ESLint Engine', () => {
diff --git a/packages/code-analyzer-eslint-engine/test/test-data/rules_ReactConfig.goldfile.json b/packages/code-analyzer-eslint-engine/test/test-data/rules_ReactConfig.goldfile.json
index f411767e..a418f570 100644
--- a/packages/code-analyzer-eslint-engine/test/test-data/rules_ReactConfig.goldfile.json
+++ b/packages/code-analyzer-eslint-engine/test/test-data/rules_ReactConfig.goldfile.json
@@ -1,4 +1,34 @@
[
+ {
+ "description": "verifies the list of dependencies for Hooks like useEffect and similar",
+ "name": "react-hooks/exhaustive-deps",
+ "resourceUrls": [
+ "https://github.com/facebook/react/issues/14920"
+ ],
+ "severityLevel": 3,
+ "tags": [
+ "Recommended",
+ "React",
+ "Design",
+ "JavaScript",
+ "TypeScript"
+ ]
+ },
+ {
+ "description": "enforces the Rules of Hooks",
+ "name": "react-hooks/rules-of-hooks",
+ "resourceUrls": [
+ "https://react.dev/reference/rules/rules-of-hooks"
+ ],
+ "severityLevel": 2,
+ "tags": [
+ "Recommended",
+ "React",
+ "Design",
+ "JavaScript",
+ "TypeScript"
+ ]
+ },
{
"description": "Enforces consistent naming for boolean props",
"name": "react/boolean-prop-naming",
@@ -705,21 +735,6 @@
"TypeScript"
]
},
- {
- "description": "Disallow React to be incorrectly marked as unused",
- "name": "react/jsx-uses-react",
- "resourceUrls": [
- "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/jsx-uses-react.md"
- ],
- "severityLevel": 3,
- "tags": [
- "Recommended",
- "React",
- "Design",
- "JavaScript",
- "TypeScript"
- ]
- },
{
"description": "Disallow variables used in JSX to be incorrectly marked as unused",
"name": "react/jsx-uses-vars",
@@ -1264,21 +1279,6 @@
"TypeScript"
]
},
- {
- "description": "Disallow missing React when using JSX",
- "name": "react/react-in-jsx-scope",
- "resourceUrls": [
- "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/react-in-jsx-scope.md"
- ],
- "severityLevel": 3,
- "tags": [
- "Recommended",
- "React",
- "Design",
- "JavaScript",
- "TypeScript"
- ]
- },
{
"description": "Enforce a defaultProps definition for every prop that is not a required prop",
"name": "react/require-default-props",
diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactFiles/HooksViolations.jsx b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactFiles/HooksViolations.jsx
new file mode 100644
index 00000000..e51eb940
--- /dev/null
+++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactFiles/HooksViolations.jsx
@@ -0,0 +1,103 @@
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+
+// ============================================================================
+// VIOLATIONS OF: react-hooks/rules-of-hooks
+// Rule: Hooks must be called at the top level, not inside conditions/loops
+// ============================================================================
+
+function ComponentWithConditionalHook({ showCounter }) {
+ // VIOLATION: Hook called inside a condition
+ if (showCounter) {
+ const [count, setCount] = useState(0);
+ }
+
+ return
Component
;
+}
+
+function ComponentWithLoopHook({ items }) {
+ // VIOLATION: Hook called inside a loop
+ for (let i = 0; i < items.length; i++) {
+ const [itemState, setItemState] = useState(items[i]);
+ }
+
+ return
Component with loop
;
+}
+
+// VIOLATION: Hook called in a regular function (not a component or custom hook)
+function regularFunction() {
+ const [value, setValue] = useState('test');
+ return value;
+}
+
+// ============================================================================
+// CODE THAT WOULD TRIGGER LWC RULES (but shouldn't since this is a React file)
+// @lwc/lwc/no-inner-html - LWC rule that disallows innerHTML usage
+// This code uses innerHTML which would be flagged by LWC rules, but since
+// .jsx files are React files (not LWC), this rule should NOT be applied.
+// ============================================================================
+
+function ComponentWithInnerHTML() {
+ const ref = React.useRef(null);
+
+ useEffect(() => {
+ // This would trigger @lwc/lwc/no-inner-html if LWC rules were applied
+ if (ref.current) {
+ ref.current.innerHTML = 'Dynamic content';
+ }
+ }, []);
+
+ return ;
+}
+
+// ============================================================================
+// VIOLATIONS OF: react-hooks/exhaustive-deps
+// Rule: Dependencies array must include all values used inside the effect
+// ============================================================================
+
+function ComponentWithMissingDeps({ userId, config }) {
+ const [userData, setUserData] = useState(null);
+ const [count, setCount] = useState(0);
+
+ // VIOLATION: 'userId' is used but not in dependencies
+ useEffect(() => {
+ fetch(`/api/users/${userId}`)
+ .then(res => res.json())
+ .then(data => setUserData(data));
+ }, []); // Missing 'userId' in dependency array
+
+ // VIOLATION: 'config' is used but not in dependencies
+ useEffect(() => {
+ console.log('Config changed:', config.theme);
+ document.body.className = config.theme;
+ }, []); // Missing 'config' or 'config.theme' in dependency array
+
+ // VIOLATION: 'count' is used but not in dependencies
+ const handleClick = useCallback(() => {
+ console.log('Current count:', count);
+ setCount(count + 1);
+ }, []); // Missing 'count' in dependency array
+
+ // VIOLATION: 'userData' is used but not in dependencies
+ const processedData = useMemo(() => {
+ if (!userData) return null;
+ return {
+ ...userData,
+ displayName: userData.firstName + ' ' + userData.lastName
+ };
+ }, []); // Missing 'userData' in dependency array
+
+ return (
+