JS-2407 Implement rule S9383: Promises should not be left unhandled by martin-strecker-sonarsource · Pull Request #7896 · SonarSource/SonarJS · GitHub
Skip to content

JS-2407 Implement rule S9383: Promises should not be left unhandled - #7896

Open
martin-strecker-sonarsource wants to merge 10 commits into
masterfrom
Martin/JS-2407_ImplementS9383FloatingPromises
Open

martin-strecker-sonarsource wants to merge 10 commits into
masterfrom
Martin/JS-2407_ImplementS9383FloatingPromises

Conversation

@martin-strecker-sonarsource

@martin-strecker-sonarsource martin-strecker-sonarsource commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Part of JS-2407

Summary

  • Implements S9383, wrapping typescript-eslint's no-floating-promises the same way S6544 wraps no-misused-promises.
  • Default options are inherited from upstream unchanged (ignoreVoid: true, checkThenables: false, ignoreIIFE: false, allowForKnownSafeCalls/allowForKnownSafePromises empty).
  • Upstream's "add await"/"add void" suggestions are passed through unmodified (not stripped), following the established pattern for decorated wraps of suggestion-producing typescript-eslint rules (e.g. S6572, S6582). RSPEC declares quickfix: covered to match this codebase's validate-quickfix.ts, which only accepts covered as the pass state for hasSuggestions=true (not partial, even though that's also a documented RSPEC value).
  • Fixes a false positive found during the manual ruling review directly in the decorator: upstream's isValidRejectionHandler() misclassifies an any-typed rejection handler as "not a function", since any structurally has zero call signatures despite being callable at runtime. Filed typescript-eslint/typescript-eslint#12848 with a proposed upstream fix — closed by the maintainer as working-as-intended (deliberate FP/FN tradeoff on their side, not a bug). S9383/rule.ts now intercepts the two floatingUselessRejectionHandler(Void) messageIds and suppresses the report locally when the handler's resolved type is exactly any, covering all 4 real-world occurrences from the ruling review (see follow-up comment below for details).
  • RSPEC is in review: SonarSource/rspec#8128. A local RSPEC mirror (S9383.json) is included so the rule builds and rulings pass before that PR merges; it should be superseded automatically once the RSPEC sync job picks up the merged rule.
  • Root rspec.sha is temporarily pinned, per docs/BUILD.md#baseline-ci-mismatches / docs/DEV.md, so CI's prepare_rspec_rule_data job fetches the real S9383 metadata instead of failing (it isn't on the RSPEC default branch yet). The pin points at a merge commit combining the current dogfood-automerge tip (the actual default RSPEC branch this tooling resolves against, not master) with the S9383 RSPEC branch, pushed to a throwaway Martin/dogfood-pin-S9383 branch on the rspec repo so PR #8128's own history stays clean. An earlier master-based pin caused CI to fail with ENOENT on an unrelated rule's data file (S7503.json) because it was missing whatever dogfood-automerge has that master doesn't yet. Must be removed before merging to master, and re-pinned again if dogfood-automerge moves further before this merges.

Ruling

  • Ran the full ruling suite; every project producing an S9383 hit had a new baseline added (25 files across 24 projects, 791 total findings).
  • Spot-checked the two highest-volume projects before syncing:
    • desktop (360 issues/76 files): unawaited async conditionalVersion(...) calls inside DB constructors (Dexie migration setup) — real fire-and-forget async work.
    • eigen (257 issues/186 files): spread ~1/file, e.g. unawaited async calls inside useEffect and unawaited navigate(...) in ternaries — genuine floating promises, not a detection bug.
  • npm run ruling is green (59/59) after ruling-sync; re-verified green after the suggestion pass-through change (locations/messages unaffected).
  • Full manual TP/FP review of all 791 ruling findings is in progress; will post results as a follow-up comment.

Test plan

  • npx tsx --test packages/analysis/src/jsts/rules/S9383/cb.test.ts passes (12 comment-based scenarios: floating statement, awaited, returned, .catch(), .then() with/without rejection handler, void-ignored, floating promise array vs. Promise.all(), floating async IIFE, any-typed rejection handler (not reported), non-function rejection handler (still reported)), including exact quick-fix output assertions ([[qfN]]/fix@/edit@) for the scenarios that carry suggestions.
  • npm run bbf builds clean.
  • mvn install -DskipTests builds clean (validated validate-quickfix and RSPEC refresh against the repinned commit).
  • npm run ruling green after baseline sync.
  • Merge/sync SonarSource/rspec#8128 before merging this PR.
  • Remove root rspec.sha pin before merging to master.

🤖 Generated with Claude Code

Wraps typescript-eslint's no-floating-promises, following the S6544
wrapping pattern. Suggestions (add await/void) are stripped for now
since quickfix support isn't implemented yet.
New expected ruling output for S9383 across the JS/TS ruling corpus,
generated with npm run ruling / ruling-sync. Spot-checked the two
highest-volume projects (desktop: 360 issues, eigen: 257 issues)
against source - both are genuine fire-and-forget async calls
(unawaited async methods in constructors, unawaited calls in
useEffect/JSX handlers), not a false-positive pattern.
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 7, 2026

Copy link
Copy Markdown

Pins rspec.sha to SonarSource/rspec#8128 (still unmerged) so CI's
prepare_rspec_rule_data job fetches the real RSPEC metadata for S9383
instead of failing with ENOENT on resources/rule-data/javascript/S9383.json,
per docs/BUILD.md's documented process for a SonarJS PR that depends on
an in-flight RSPEC change. Remove this file once SonarSource/rspec#8128
merges to the RSPEC default branch.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Ruling Report

New issues flagged (791 issues)

S9383

ace/src/ext/code_lens_test.js:114

   112 |     "test async code lens": function(next) {
   113 |         editor.session.setValue("a\nb\nc");
>  114 |         new Promise(function(resolve) {
   115 |                 codeLens.registerCodeLensProvider(editor, {
   116 |                     provideCodeLenses: function(session, callback) {

ace/src/ext/code_lens_test.js:139

   137 |     "test multiple code lens providers": function(next) {
   138 |         editor.session.setValue("a\nb\nc\nd");
>  139 |         new Promise(function(resolve) {
   140 |                 codeLens.registerCodeLensProvider(editor, {
   141 |                     provideCodeLenses: function(session, callback) {

ace/src/keyboard/vim.js:2066

  2064 |         }
  2065 |         if (registerName === '+') {
> 2066 |           navigator.clipboard.writeText(text);
  2067 |         }
  2068 |         // The unnamed register always has the same value as the last used

ace/src/keyboard/vim.js:3602

  3600 |             actionArgs.registerName);
  3601 |         if (actionArgs.registerName === '+') {
> 3602 |           navigator.clipboard.readText().then((value) => {
  3603 |             this.continuePaste(cm, actionArgs, vim, value, register);
  3604 |           })

ace/src/mouse/touch_handler.js:50

    48 |             }
    49 |             if (action == "paste") {
>   50 |                 clipboard.readText().then(function (text) {
    51 |                     editor.execCommand(action, text);
    52 |                 });

ace/src/mouse/touch_handler.js:57

    55 |                 if (action == "cut" || action == "copy") {
    56 |                     if (clipboard)
>   57 |                         clipboard.writeText(editor.getCopyText());
    58 |                     else
    59 |                         document.execCommand("copy");

animate.css/docs/modules/playground.mjs:41

    39 |     if (el.classList.contains('copy-icon')) {
    40 |       const animation = `animate__${el.parentElement.getAttribute('data-animation')}`;
>   41 |       navigator.clipboard.writeText(animation);
    42 |     }
    43 |   });

ant-design/components/form/tests/list-noStyle.test.tsx:48

    46 |     }
    47 | 
>   48 |     addItem();
    49 |     addItem();
    50 | 

ant-design/components/form/tests/list-noStyle.test.tsx:49

    47 | 
    48 |     addItem();
>   49 |     addItem();
    50 | 
    51 |     // Submit

ant-design/components/notification/index.tsx:161

   159 | 
   160 |   if (cacheInstance) {
>  161 |     Promise.resolve(cacheInstance).then(instance => {
   162 |       callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
   163 |     });

...and 781 more

📋 View full report

New issues flagged (791)

S9383

Comment thread rspec.sha Outdated
Comment thread rspec.sha Outdated
@datadog-sonarsource

This comment has been minimized.

Stop stripping the "add await"/"add void" suggestions from
no-floating-promises. Following the established pattern for decorated
wraps of suggestion-producing typescript-eslint rules (e.g. S6572,
S6582): pass the upstream suggest payload through unmodified and test
the exact fix output via the [[qfN]]/fix@/edit@ comment-based DSL,
instead of defensively discarding it.

RSPEC now declares quickfix=partial (not all report shapes carry a
suggestion - the promise-array case never does), updated on both the
in-review RSPEC PR and the local mirror.
Comment on lines +26 to +31
export const rule: Rule.RuleModule = {
meta: generateMeta(meta, { ...noFloatingPromisesRule.meta }),
create(context: Rule.RuleContext) {
return noFloatingPromisesRule.create(context);
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: PR description still claims suggestions are stripped and quickfix unknown

The PR summary states "Suggestions (Add await operator. / Add void operator to ignore.) are stripped for now — quickfix support isn't implemented yet (RSPEC quickfix: unknown)", but this commit removes the interceptor that stripped them and flips the RSPEC mirror to partial, so suggestions are now passed through and asserted in cb.fixture.ts. Update the description (and the linked RSPEC PR's quickfix value) so reviewers and the RSPEC sync are not driven by the stale claim.

Was this helpful? React with 👍 / 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description updated

gitar-bot[bot]

This comment was marked as resolved.

…overed

The previous rspec.sha pin was a commit based on plain master, but
CI's unpinned default (and the rspec-maven-plugin's rule-data
generation) resolves against dogfood-automerge, which has diverged
from master. Pinning to master-only meant deploy-rule-data couldn't
find resources/rule-data/javascript/S7503.json (unrelated rule) and
prepare_rspec_rule_data failed with ENOENT.

Fix: merge the RSPEC S9383 branch onto the current dogfood-automerge
tip (SonarSource/rspec@2d0508b, via a throwaway
Martin/dogfood-pin-S9383 branch so PR #8128's own history isn't
rewritten) and pin rspec.sha to that commit instead. Verified locally
with `npm run rspec:refresh` - resolves cleanly now, no ENOENT.

Also mirrors the RSPEC-side quickfix=covered fix (see previous commit)
in the local resource file, confirmed by the live refresh pulling the
same value from the pinned commit.
@gitar-bot
gitar-bot Bot dismissed their stale review September 8, 2026 12:49

✅ Code review updated (blocking issues remain unresolved).

Configure merge blocking

@sonarqube-next

sonarqube-next Bot commented Sep 8, 2026

Copy link
Copy Markdown

@martin-strecker-sonarsource

Copy link
Copy Markdown
Contributor Author

S9383 ruling validation — 791 findings

Count %
True Positive 775 98.0%
Confirmed False Positive 6 0.8%
Borderline / low-value TP (not a defect — see below) 10 1.3%

Confirmed false positives — 2 distinct root causes, both pre-existing upstream limitations (not introduced by our wrapping)

Root cause A — any-typed rejection handler misread as "not a function" (4 instances)
Upstream's isValidRejectionHandler() checks getCallSignatures().length > 0 on the handler's resolved TS type. any structurally has zero call signatures even though it's callable at runtime by definition, so a genuinely-callable handler gets misreported as "A rejection handler that is not a function will be ignored."
Not previously tracked upstream (checked) — filed typescript-eslint/typescript-eslint#12848 with a verified minimal repro and a proposed one-line fix (tsutils.isIntrinsicAnyType short-circuit, using a helper already imported in that file), confirmed locally to resolve all four occurrences without weakening detection of genuinely non-callable handlers.

  • ace/src/ext/code_lens_test.js:114 and :139.catch(next), untyped Mocha callback parameter.
  • p5.js/src/core/preload.js:93.catch(errorCallback), untyped rest-args reassignment.
  • eigen/scripts/algolia-example-galleries-near-me.ts:82.catch(console.error); project's tsconfig.json has no "node"/DOM lib, so console is untyped there.

Root cause B — array-mutation method misread as a floating promise array (2 instances)

  • vitest/packages/expect/src/utils.ts:36 and vitest/packages/runner/src/artifact.ts:148.splice() on an already-.finally()-handled Promise[] tracking array. .splice()'s return type is coincidentally Promise[] too, which trips the array-of-promises heuristic even though nothing new is being launched.

Borderline cases (10) — technically true positives per the rule's literal contract, but low real-world signal

  • vuetify/packages/vuetify/src/components/VOverlay/useActivator.tsx — timer-backed promises whose executor structurally never calls reject (verified). Matches the exception our own rule.adoc already documents ("can be ignored for promises you know will always resolve like timers").
  • oak/http_server_bun.ts:210 — flagged from an interface's declared void | Promise<void> union; this concrete override never returns a promise at runtime.
  • vitest/packages/vitest/src/runtime/moduleRunner/nativeModuleMocker.ts:199 — the derived .finally() chain is technically unhandled even though the original promise is awaited by the caller elsewhere.

Non-issue worth a one-line mention

ant-design/components/form/__tests__/list-noStyle.test.tsx:48-49 — correct TP, but S9383 is scope: Main; a real SonarQube scan excludes test files at the Java-plugin level. The ruling harness doesn't apply that filter, so it surfaces here but wouldn't in production.

Bottom line: zero false positives traceable to our wrapper — both FP categories are pre-existing upstream typescript-eslint limitations (one now filed and fix-proposed), and at 6/791 (0.8%) they're narrow enough that I wouldn't block on them.

🤖 Generated with Claude Code

@martin-strecker-sonarsource

Copy link
Copy Markdown
Contributor Author

Re: #7896 (comment)

Checked both points against current state rather than the diff snapshot the bot analyzed:

Point 2 (description contradicts implementation) — already stale. The PR description was updated two commits ago to reflect that suggestions are passed through (not stripped) and RSPEC declares quickfix: covered (not partial/unknown). No further action needed there.

Point 1 (no automated guard against rspec.sha reaching master) — real, but the framing is slightly off. A guard already exists: the "Reject root rspec.sha" step inside the promote job (if [ -e rspec.sha ]; then exit 1; fi) — it's currently failing on this PR, exactly as intended. So it's not that no CI step exists.

The actual gap: I checked this repo's branch-protection rulesets via the API (gh api repos/SonarSource/SonarJS/rulesets/<id>), and promote is not a required status check on master — the only required check is StepSecurity Required Checks (a security scan, unrelated). So nothing currently stops a merge while that step is red; the existing guard just isn't wired up as required.

That's a repo-level branch-protection/ruleset setting, not something a code change in this PR's diff can fix — happy to raise it separately with whoever administers branch protection for this repo, since it'd protect every future rspec.sha pin, not just this one. For this PR specifically, the pin removal is already tracked as an explicit checklist item in the description.

Upstream's isValidRejectionHandler() treats any as "not a function" since it
structurally has zero call signatures, even though it's callable at runtime.
typescript-eslint/typescript-eslint#12848 was closed working-as-intended, so
fix it locally: interceptReport() re-checks the handler's type and drops the
report when it's exactly any, covering the 4 real-world FPs from ruling.
@gitar-bot

gitar-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 4 resolved / 5 findings

Implements S9383 (unhandled promises) as a decorated wrap of typescript-eslint's no-floating-promises, passing through upstream suggestions for "add await" and "add void" with quickfix: covered. Update the PR description to reflect that suggestions are now passed through (not stripped) and verify the RSPEC mirror and root rspec.sha pin are removed before merging to master.

💡 Quality: PR description still claims suggestions are stripped and quickfix unknown

📄 packages/analysis/src/jsts/rules/S9383/rule.ts:26-31 📄 packages/analysis/src/jsts/rules/S9383/cb.fixture.ts:5-9 📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25

The PR summary states "Suggestions (Add await operator. / Add void operator to ignore.) are stripped for now — quickfix support isn't implemented yet (RSPEC quickfix: unknown)", but this commit removes the interceptor that stripped them and flips the RSPEC mirror to partial, so suggestions are now passed through and asserted in cb.fixture.ts. Update the description (and the linked RSPEC PR's quickfix value) so reviewers and the RSPEC sync are not driven by the stale claim.

✅ 4 resolved
Bug: Local S9383 RSPEC mirror is wiped by rspec:refresh, breaking CI

📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:1-15 📄 packages/analysis/src/jsts/rules/S9383/meta.ts:18-22
The hand-written S9383.json mirror does not survive the RSPEC sync path that CI runs, so the build breaks until rspec#8128 is merged and rspec.sha bumped. .github/workflows/build.yml job prepare_rspec_rule_data runs npm run rspec:refresh, which downloads rspec rule data into resources/rule-data/javascript and then calls npm run deploy-rule-data. syncRuleData() derives its rule list from the rule folders (listRulesDir(), which now contains S9383), rmSynces the whole .../l10n/javascript/rules/javascript folder — deleting the committed S9383.json — and then, for every rule name, reads <source>/S9383.json and copies <source>/S9383.html. Both are absent while the RSPEC PR is open, so writeNormalizedManifest/copyFileSync throw ENOENT and every downstream build/test job fails; even a tolerant sync would leave the rule with no S9383.html, which CheckListTest#test asserts exists for every rule key. So the PR description's claim that the local mirror lets the rule build before rspec merges only holds for npm run bbf (which reads the json through getRspecMeta), not for the Maven/CI path — keep this PR blocked on the RSPEC merge, or make deploy-rule-data tolerate rules that have no rspec source by preserving the existing manifest/description instead of throwing.

Bug: Root rspec.sha pin overrides the dogfood-automerge RSPEC branch

📄 rspec.sha:1
The refresh is configured to resolve rule data from RSPEC branch dogfood-automerge (pom.xml:80), and docs/DEV.md:82-84 state that a root rspec.sha wins over the configured branch and that branch selection is ignored while the file is present. So this pin does not just add S9383 — it moves the RSPEC revision for all ~400 rules to the tip of the rspec#8128 PR branch. If that commit does not contain the dogfood-automerge state (a PR branch cut from rspec master will not), prepare_rspec_rule_data regenerates every rule from master-state metadata, and any rule whose RSPEC entry exists only on dogfood-automerge makes deploy-rule-data throw ENOENT in writeNormalizedManifest (tools/deploy-rule-data.ts:120,163) — the exact failure mode the pin was meant to avoid, just for a different rule. Pin a commit that merges the RSPEC S9383 branch into dogfood-automerge (or rebase the RSPEC branch onto it) so the pinned revision is a superset of the branch the repo normally refreshes from.

Bug: quickfix 'partial' + hasSuggestions fails validate-quickfix build step

📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25 📄 packages/analysis/src/jsts/rules/S9383/rule.ts:27
tools/validate-quickfix.ts:29-46 treats a rule as declaring quickfix support only when rspecMeta.quickfix === 'covered', and errors with Rule has hasSuggestions but RSPEC doesn't declare quickfix='covered' otherwise. This commit drops hasSuggestions: false from generateMeta(...), so S9383's merged meta now inherits hasSuggestions: true from typescript-eslint's no-floating-promises (node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-floating-promises.js:56), while S9383.json sets quickfix: "partial" — Validation 2 fires and the script exits 1. That script runs as part of the Maven build (docs/BUILD.md lists npm run validate-quickfix under the phases skipped only by -Dskip-nodejs), so the build/CI breaks. Note that CI refreshes RSPEC from the pinned rspec.sha, so the RSPEC PR must also declare quickfix: "covered"; setting only the local mirror to partial cannot pass either.

Quality: Committed temporary pin has no automated guard against reaching master

📄 rspec.sha:1
The root rspec.sha is listed in .gitignore:20, so it had to be force-added and will never show up in git status again — the repo's only friction against the pin reaching master is bypassed, leaving the description checkbox as the sole protection. If it merges, the nightly generated_files_freshness job (build.yml:324-352) consumes the pinned prepare_rspec_rule_data output and opens PRs rewriting every tracked rule JSON at that unmerged RSPEC PR commit, and packaged per-language rspec.sha release markers would attribute releases to a commit that exists only on an RSPEC PR branch (docs/rspec-sync-requirements.md file-roles section). Add a merge-blocking CI step that fails when rspec.sha is tracked and the PR base is master (e.g. git ls-files --error-unmatch rspec.sha && exit 1 in a PR-only job), so removal cannot be forgotten.

🤖 Prompt for agents
Code Review: Implements S9383 (unhandled promises) as a decorated wrap of typescript-eslint's `no-floating-promises`, passing through upstream suggestions for "add await" and "add void" with `quickfix: covered`. Update the PR description to reflect that suggestions are now passed through (not stripped) and verify the RSPEC mirror and root `rspec.sha` pin are removed before merging to master.

1. 💡 Quality: PR description still claims suggestions are stripped and quickfix unknown
   Files: packages/analysis/src/jsts/rules/S9383/rule.ts:26-31, packages/analysis/src/jsts/rules/S9383/cb.fixture.ts:5-9, sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25

   The PR summary states "Suggestions (`Add await operator.` / `Add void operator to ignore.`) are stripped for now — quickfix support isn't implemented yet (RSPEC `quickfix: unknown`)", but this commit removes the interceptor that stripped them and flips the RSPEC mirror to `partial`, so suggestions are now passed through and asserted in `cb.fixture.ts`. Update the description (and the linked RSPEC PR's `quickfix` value) so reviewers and the RSPEC sync are not driven by the stale claim.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

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

Was this helpful? React with 👍 / 👎 | Gitar

@martin-strecker-sonarsource

Copy link
Copy Markdown
Contributor Author

Update (2026-09-10): typescript-eslint/typescript-eslint#12848 (filed above for root cause A) was closed by the maintainer (bradzacher) as NOT_PLANNED/working-as-intended — treating an any-typed rejection handler as reportable is a deliberate FP/FN tradeoff on their side, not a bug they're planning to fix. Since this is now a permanent upstream characteristic rather than a pending fix, root cause A's 4 false positives are suppressed locally in this PR's decorator instead: S9383/rule.ts intercepts the floatingUselessRejectionHandler(Void) messageIds and drops the report when the rejection handler's resolved type is exactly any. Root cause B (the .splice()-on-tracking-array FPs) is unaffected by this and remains open.

Drop optional-chaining and void-unwrapping - none of the 4 real-world FPs
from the ruling review took those shapes, and the decorator shouldn't
guess at forms we haven't actually seen.
Removes the 4 false positives fixed in the decorator: ace:114,139,
p5.js:93, eigen:82 - the exact any-typed .catch() handlers from the
ruling review.

@guillemsarda guillemsarda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Worth adding the two tests.

}

// `any` has no call signatures though it's callable at runtime; upstream closed this
// as working-as-intended (typescript-eslint/typescript-eslint#12848), so fix it here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the Issue!

// edit@qf6 {{ await (async () =>}}
await fetchData();
})();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding two regression tests for the Optional chaining and Computed member access cases.

Optional chaining:

declare function fetchData(): Promise<void>;

function handle(next: any) {
  fetchData()?.catch(next);
}

declare const maybePromise: Promise<void> | undefined;

function handle(next: any) {
  maybePromise?.then(undefined, next);
}

Computed member access:

declare function fetchData(): Promise<void>;

function handle(next: any) {
  fetchData()['catch'](next);
}

function handle(next: any) {
  fetchData()[`then`](undefined, next);
}

@@ -0,0 +1,99 @@
/*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Optional) This rule puts the decoration logic in rule.ts, but the established convention across the codebase (~95 decorated rules) is to split it into a dedicated decorator.ts, with index.ts doing export const rule = decorate(getBaseRule(...)). Only a few rules (S6544, S6572, S6582) currently deviate from that and inline everything in rule.ts.

Could we rename rule.tsdecorator.ts here (and adjust index.ts accordingly) to follow the majority pattern, rather than growing the outlier group to 4? Makes it easier for reviewers/future maintainers to find the FP-suppression/quick-fix logic in a consistent place across rules.

Follows the codebase's majority pattern for decorated typescript-eslint
rules (index.ts calling decorate() from decorator.ts) instead of inlining
everything in rule.ts, per review feedback.
…cess

Locks in that rejection handlers reached only through optional chaining
(?.catch/?.then) or computed member access (['catch']/[`then`]) fall
outside findRejectionHandler's direct-call match and are still reported,
even when any-typed, per review feedback.
@gitar-bot

gitar-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown
CI failed: The Maven build failed during rule data deployment due to a missing rule definition file (S9381.json) encountered by the rspec refresh script.

Overview

1 unique build failure pattern was detected across 3 failed jobs. The failure is directly related to the PR changes involving RSPEC rule integration.

Failures

Missing Rule Data File (confidence: high)

  • Type: build
  • Affected jobs: 103007246350, 103010555414, 104304460549
  • Related to change: yes
  • Root cause: The deploy-rule-data script (tools/deploy-rule-data.ts) attempted to read rule data file resources/rule-data/javascript/S9381.json, but the file does not exist (ENOENT), causing the Maven build to fail with exit code 1.
  • Suggested fix: Add the missing rule data file resources/rule-data/javascript/S9381.json or update the rule baseline/mapping so that the deploy script does not look for a non-existent rule definition file.

Summary

  • Change-related failures: 3 jobs failed due to the missing rule data file S9381.json during rule data deployment.
  • Infrastructure/flaky failures: None.
  • Recommended action: Ensure all required RSPEC rule data files (such as S9381.json) are included and properly committed in the PR.
Code Review 👍 Approved with suggestions 4 resolved / 5 findings

Implements S9383 (unhandled promises) as a decorated wrap of typescript-eslint's no-floating-promises, passing through upstream suggestions for "add await" and "add void" with quickfix: covered. Update the PR description to reflect that suggestions are now passed through (not stripped) and verify the RSPEC mirror and root rspec.sha pin are removed before merging to master.

💡 Quality: PR description still claims suggestions are stripped and quickfix unknown

📄 packages/analysis/src/jsts/rules/S9383/rule.ts:26-31 📄 packages/analysis/src/jsts/rules/S9383/cb.fixture.ts:5-9 📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25

The PR summary states "Suggestions (Add await operator. / Add void operator to ignore.) are stripped for now — quickfix support isn't implemented yet (RSPEC quickfix: unknown)", but this commit removes the interceptor that stripped them and flips the RSPEC mirror to partial, so suggestions are now passed through and asserted in cb.fixture.ts. Update the description (and the linked RSPEC PR's quickfix value) so reviewers and the RSPEC sync are not driven by the stale claim.

✅ 4 resolved
Bug: Local S9383 RSPEC mirror is wiped by rspec:refresh, breaking CI

📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:1-15 📄 packages/analysis/src/jsts/rules/S9383/meta.ts:18-22
The hand-written S9383.json mirror does not survive the RSPEC sync path that CI runs, so the build breaks until rspec#8128 is merged and rspec.sha bumped. .github/workflows/build.yml job prepare_rspec_rule_data runs npm run rspec:refresh, which downloads rspec rule data into resources/rule-data/javascript and then calls npm run deploy-rule-data. syncRuleData() derives its rule list from the rule folders (listRulesDir(), which now contains S9383), rmSynces the whole .../l10n/javascript/rules/javascript folder — deleting the committed S9383.json — and then, for every rule name, reads <source>/S9383.json and copies <source>/S9383.html. Both are absent while the RSPEC PR is open, so writeNormalizedManifest/copyFileSync throw ENOENT and every downstream build/test job fails; even a tolerant sync would leave the rule with no S9383.html, which CheckListTest#test asserts exists for every rule key. So the PR description's claim that the local mirror lets the rule build before rspec merges only holds for npm run bbf (which reads the json through getRspecMeta), not for the Maven/CI path — keep this PR blocked on the RSPEC merge, or make deploy-rule-data tolerate rules that have no rspec source by preserving the existing manifest/description instead of throwing.

Bug: Root rspec.sha pin overrides the dogfood-automerge RSPEC branch

📄 rspec.sha:1
The refresh is configured to resolve rule data from RSPEC branch dogfood-automerge (pom.xml:80), and docs/DEV.md:82-84 state that a root rspec.sha wins over the configured branch and that branch selection is ignored while the file is present. So this pin does not just add S9383 — it moves the RSPEC revision for all ~400 rules to the tip of the rspec#8128 PR branch. If that commit does not contain the dogfood-automerge state (a PR branch cut from rspec master will not), prepare_rspec_rule_data regenerates every rule from master-state metadata, and any rule whose RSPEC entry exists only on dogfood-automerge makes deploy-rule-data throw ENOENT in writeNormalizedManifest (tools/deploy-rule-data.ts:120,163) — the exact failure mode the pin was meant to avoid, just for a different rule. Pin a commit that merges the RSPEC S9383 branch into dogfood-automerge (or rebase the RSPEC branch onto it) so the pinned revision is a superset of the branch the repo normally refreshes from.

Bug: quickfix 'partial' + hasSuggestions fails validate-quickfix build step

📄 sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25 📄 packages/analysis/src/jsts/rules/S9383/rule.ts:27
tools/validate-quickfix.ts:29-46 treats a rule as declaring quickfix support only when rspecMeta.quickfix === 'covered', and errors with Rule has hasSuggestions but RSPEC doesn't declare quickfix='covered' otherwise. This commit drops hasSuggestions: false from generateMeta(...), so S9383's merged meta now inherits hasSuggestions: true from typescript-eslint's no-floating-promises (node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-floating-promises.js:56), while S9383.json sets quickfix: "partial" — Validation 2 fires and the script exits 1. That script runs as part of the Maven build (docs/BUILD.md lists npm run validate-quickfix under the phases skipped only by -Dskip-nodejs), so the build/CI breaks. Note that CI refreshes RSPEC from the pinned rspec.sha, so the RSPEC PR must also declare quickfix: "covered"; setting only the local mirror to partial cannot pass either.

Quality: Committed temporary pin has no automated guard against reaching master

📄 rspec.sha:1
The root rspec.sha is listed in .gitignore:20, so it had to be force-added and will never show up in git status again — the repo's only friction against the pin reaching master is bypassed, leaving the description checkbox as the sole protection. If it merges, the nightly generated_files_freshness job (build.yml:324-352) consumes the pinned prepare_rspec_rule_data output and opens PRs rewriting every tracked rule JSON at that unmerged RSPEC PR commit, and packaged per-language rspec.sha release markers would attribute releases to a commit that exists only on an RSPEC PR branch (docs/rspec-sync-requirements.md file-roles section). Add a merge-blocking CI step that fails when rspec.sha is tracked and the PR base is master (e.g. git ls-files --error-unmatch rspec.sha && exit 1 in a PR-only job), so removal cannot be forgotten.

🤖 Prompt for agents
Code Review: Implements S9383 (unhandled promises) as a decorated wrap of typescript-eslint's `no-floating-promises`, passing through upstream suggestions for "add await" and "add void" with `quickfix: covered`. Update the PR description to reflect that suggestions are now passed through (not stripped) and verify the RSPEC mirror and root `rspec.sha` pin are removed before merging to master.

1. 💡 Quality: PR description still claims suggestions are stripped and quickfix unknown
   Files: packages/analysis/src/jsts/rules/S9383/rule.ts:26-31, packages/analysis/src/jsts/rules/S9383/cb.fixture.ts:5-9, sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/S9383.json:25

   The PR summary states "Suggestions (`Add await operator.` / `Add void operator to ignore.`) are stripped for now — quickfix support isn't implemented yet (RSPEC `quickfix: unknown`)", but this commit removes the interceptor that stripped them and flips the RSPEC mirror to `partial`, so suggestions are now passed through and asserted in `cb.fixture.ts`. Update the description (and the linked RSPEC PR's `quickfix` value) so reviewers and the RSPEC sync are not driven by the stale claim.

Review coverage

Functional validation No results

Rules No rules evaluated

Auto-approval Not enabled · Set up

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

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

Was this helpful? React with 👍 / 👎 | Gitar

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