Skip to content

JS-1802 Fix S3801 false positives for React effect cleanups#7127

Draft
francois-mora-sonarsource wants to merge 3 commits into
masterfrom
fix/s3801-react-effect-cleanup-return
Draft

JS-1802 Fix S3801 false positives for React effect cleanups#7127
francois-mora-sonarsource wants to merge 3 commits into
masterfrom
fix/s3801-react-effect-cleanup-return

Conversation

@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor

Summary

Fixes a React false positive in S3801 by skipping callbacks passed to React effect hooks. Effect callbacks may return a cleanup function or return nothing, so an early return; mixed with return () => cleanup() is valid React code rather than inconsistent API behavior.

The exemption covers useEffect, useLayoutEffect, and useInsertionEffect imported from react, including aliased named imports and React.useEffect-style calls.

Validation

  • ./node_modules/.bin/tsx --tsconfig packages/tsconfig.test.json --test packages/analysis/src/jsts/rules/S3801/unit.test.ts
  • git diff --check

npm run bridge:compile was attempted, but this fresh worktree lacks the ignored generated rule metadata/index artifacts expected by the command, so it fails before branch-specific checking.

@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod Bot changed the title Fix S3801 false positives for React effect cleanups JS-1802 Fix S3801 false positives for React effect cleanups May 29, 2026
@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented May 29, 2026

JS-1802

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Ruling Report

Code no longer flagged (20 issues)

S3801

ant-design/components/checkbox/Checkbox.tsx:77

    75 |   }, []);
    76 | 
>   77 |   React.useEffect(() => {
    78 |     if (skipGroup) {
    79 |       return;

eigen/src/app/Components/ArtsyWebView.tsx:298

   296 | 
   297 | function useUrlCookies(url: string, accessToken: string | null, isLoggedIn: boolean) {
>  298 |   useEffect(() => {
   299 |     if (accessToken && isLoggedIn) {
   300 |       const attempt = new CookieRequestAttempt(url, accessToken)

eigen/src/app/Scenes/Artwork/Components/CommercialButtons/InquiryModal.tsx:177

   175 |   }
   176 | 
>  177 |   useEffect(() => {
   178 |     if (mutationSuccessful) {
   179 |       resetAndExit()

eigen/src/app/Scenes/Onboarding/OnboardingWelcome.tsx:54

    52 |   }, [])
    53 | 
>   54 |   useEffect(() => {
    55 |     if (Platform.OS === "ios") {
    56 |       return

eigen/src/app/Scenes/Onboarding/OnboardingWelcome.tsx:67

    65 |   }, [navigation])
    66 | 
>   67 |   useEffect(() => {
    68 |     if (Platform.OS === "ios") {
    69 |       return

eigen/src/app/Scenes/Search/components/SearchInput.tsx:38

    36 |   )
    37 | 
>   38 |   useEffect(() => {
    39 |     if (searchProviderValues.inputRef?.current && isAndroid) {
    40 |       const unsubscribe = navigation?.addListener("focus", () => {

eigen/src/app/Websockets/auctions/AuctionSocketContext.tsx:50

    48 |   const { cable, channelsHolder } = useCable()
    49 | 
>   50 |   useEffect(() => {
    51 |     if (!enabled) {
    52 |       return

eigen/src/app/utils/AboveTheFoldQueryRenderer.tsx:90

    88 |   const [renderBelowTheFold, setRenderBelowTheFold] = useState(false)
    89 | 
>   90 |   useEffect(() => {
    91 |     if (props.belowTheFoldTimeout === undefined) {
    92 |       return

eigen/src/palette/elements/Input/Input.tsx:127

   125 |     const fontFamily = theme.fonts.sans.regular
   126 | 
>  127 |     useEffect(() => {
   128 |       if (!addClearListener) {
   129 |         return

fireact/src/pages/auth/user/Invite/index.js:23

    21 |     const { setBreadcrumb } = useContext(BreadcrumbContext);
    22 | 
>   23 |     useEffect(() => {
    24 |         setBreadcrumb([
    25 |             {

...and 10 more

📋 View full report

Code no longer flagged (20)

S3801

github-actions Bot and others added 2 commits May 29, 2026 13:54
🤖 Generated with GitHub Actions
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented May 29, 2026

Code Review ✅ Approved

Updates rule S3801 to exempt React effect hook callbacks, correctly allowing both cleanup functions and early returns in useEffect and related hooks. No issues found.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqube-next
Copy link
Copy Markdown

@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor Author

Rule Profile

Field Value
Rule S3801 - Functions should use return consistently
Severity / type Major CODE_SMELL
Sonar Way No
Scope Main
function getValue(condition) {
  if (condition) {
    return 42;
  }
  return;
}

False Positive Pattern

The false positives happen in React effect callbacks that return a cleanup function on one path and return nothing on another path. S3801 normally treats this as inconsistent API behavior, but React gives effect callback returns a special meaning: returning a function registers cleanup, while returning nothing means there is no cleanup for that effect run.

useEffect(() => {
  if (!enabled) {
    return;
  }

  const subscription = subscribe();
  return () => subscription.unsubscribe();
}, [enabled]);

False Negative Risk

The risk is bounded because the fix does not suppress all functions with mixed returns, nor all callbacks. It suppresses only the first argument passed to useEffect, useLayoutEffect, or useInsertionEffect when those hooks are imported from react, including named aliases and React.useEffect-style calls. Normal functions, callbacks passed to other APIs, and untracked custom wrappers are still checked by S3801.

Fix Summary

The implementation tracks React imports, recognizes React effect hook calls, and skips S3801 reporting for the effect callback itself. This matches the React contract where cleanup returns are optional and should not be interpreted as inconsistent function return behavior.

  • Tracks named React effect hook imports, aliased imports, default imports, and namespace imports.
  • Recognizes direct hook calls and React.useEffect-style member calls.
  • Adds regression tests for useEffect, useLayoutEffect, useInsertionEffect, React.useEffect, and aliased useEffect imports.
  • Updates ruling snapshots for the 20 React effect false positives no longer reported.

@nathsou
Copy link
Copy Markdown
Contributor

nathsou commented May 29, 2026

For TypeScript, maybe this FP could be more general, I think it applies to any function that returns a union of (either undefined or void) and another type.

See the signature for the useEffect callback argument: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L1665

Here the return type is void | Destructor, so an empty return; or return undefined; should be considered valid even if another return statement returns something else (granted we have type information)

@nathsou nathsou requested review from nathsou and removed request for nathsou May 29, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants