Fix FP on S7739: accept explicit thenable-contract classes (JS-1821) - #7863
francois-mora-sonarsource wants to merge 12 commits into
Conversation
Classes that explicitly declare a thenable contract (JSDoc @implements {IThenable}/{Thenable}, or TypeScript implements PromiseLike<T>) no longer get reported for defining a then() method. The containing class is resolved via the repo's existing getAncestorsWithParent helper (nearest-first) rather than context.sourceCode.getAncestors (outermost-first, as used by the community-proposed fix attached to the Jira), so a then() method on an unannotated inner class nested inside an annotated outer class is still correctly reported. Jira: JS-1821
Ruling ReportNo changes to ruling expected issues in this PR |
…egatives - isClassThenMethodWithThenableContract now also recognizes PropertyDefinition (class field) 'then' members and statically-known string keys, matching how the upstream unicorn rule resolves the reported key. Previously only Identifier-keyed MethodDefinition members were exempted, so a thenable- contract class field was still incorrectly reported. - hasTypeScriptThenableContract now matches only the heritage entry's implemented interface name, not its full text (which included type arguments). Previously `implements Cache<PromiseLike<Data>>` was mistaken for an explicit thenable contract, suppressing a genuine report. Jira: JS-1821
…tract
Non-blocking follow-up from PR review: cover the quoted string key
('then'() {}) and TypeScript class-field (then = ...) shapes that the
existing suppression logic already handles but weren't exercised by tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Scope the JSDoc @implements match to the braced type name so unrelated prose on the same comment line (e.g. mentioning "PromiseLike") can no longer suppress a genuine report (false negative). - Recognize a JSDoc thenable contract on a class expression assigned via const/let/var (e.g. `const Foo = class {...}`), not just export-wrapped class declarations, since the comment precedes the declaration rather than the `class` keyword (residual false positive). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Scope the JSDoc @implements thenable-contract match to the named type only, excluding type arguments, so a class implementing an unrelated generic interface parameterized by PromiseLike/Thenable (e.g. Cache<PromiseLike<T>>) isn't mistaken for a genuine contract. - Reuse isStringLiteral instead of a hand-rolled Literal check in isThenMemberKey. - Cache the per-class thenable-contract check so it isn't recomputed for every then-named member on the same class.
An instance-side thenable contract (JSDoc @implements or TS `implements PromiseLike<T>`) was also suppressing reports on a static `then`, but a static then makes the class object itself thenable regardless of the instance contract, so it must still be reported. Excludes static members in isClassThenMethodWithThenableContract and adds regression tests (static method and static field, JS and TS).
The FP-remediation logic for the no-thenable exceptions had grown to 554 lines inline in rule.ts. Split following the S6747 convention: rule.ts now only wires the upstream rule through decorate(); decorator.ts holds the interceptReport wiring; false-positives/index.ts holds the thenable-specific exception detection; helpers.ts holds the generic, non-FP-specific AST utilities. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
getPropertyKeyName is only used internally by collectPropertyNames in the same file, so it doesn't need to be exported. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…exception isThenMemberKey required an Identifier or string Literal key, so a computed key resolved statically by the upstream unicorn rule (['then'], [`then`], or [KEY] where KEY = 'then') still bypassed the thenable-contract exception even on an annotated class. Since unicorn only ever reports a class member key it has already resolved to 'then', checking member.key === node is sufficient for all key shapes.
|
@gitar-bot the automated "Implementation Status" check above marks all 4 of JS-1821's "What" bullets as covered — two of those aren't actually addressed by this PR:
I've updated the PR description with an explicit "Out of scope" section to make this boundary clear for reviewers. Also fixed the still-open computed- |
|
Thanks — all 5 findings resolved and acknowledged. Note the "Implementation Status" box above is auto-regenerated each review and still incorrectly marks all 4 of JS-1821's "What" bullets as covered by this PR. As noted earlier: this PR only adds the explicit
Flagging again so reviewers don't read this PR as closing those two. |
Separates the Yup/Joi validation-library exception (exception-libraries.ts) from the structural/contract-based thenable exceptions (intentional-thenable.ts), matching the multi-file false-positives convention used elsewhere (S6819, S1848, S6767). index.ts is now a thin barrel re-exporting both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…act branches
- Fix nested-class valid-case comment that overclaimed outer/inner precedence
it didn't actually test (the real leak-prevention check is the invalid
counterpart already in the suite).
- Add coverage for branches the PR's own implementation added but no test
exercised: bare `@implements {IThenable}`/`implements PromiseLike` without
a generic argument, `export default class`, and a multi-entry TS heritage
clause.
- Drop one of three redundant computed-then-key valid cases: key-shape
resolution is delegated entirely to the upstream unicorn rule, so they all
exercised the identical code path.
Add a valid case with two 'then'-named members (accessor pair) on one annotated class, so the per-class contract cache in hasExplicitThenableContract is actually consulted twice instead of always short-circuiting on the first lookup.
Code Review ✅ Approved 6 resolved / 6 findingsFixes false positives on S7739 for classes implementing ✅ 6 resolved✅ Bug: Thenable-contract exception misses class fields named
|
| Auto-apply | Compact |
|
|
Was this helpful? React with 👍 / 👎 | Gitar
|





Jira
JS-1821 — Fix FP on S7739: classes implementing
IThenableinterfaces flagged incorrectly.Paired analyzer keys
javascript:S7739,typescript:S7739Boundary
Suppressed: a class carrying a
then()method is not reported when that same class (thenearest enclosing class, not the outermost) declares an explicit thenable contract:
@implements {IThenable}/{IThenable<T>}/{Thenable}(on the class itself or onthe
export class/export default classwrapper), orimplements PromiseLike<T>.Still reported:
then()method with no thenable-contract annotation;@implements(e.g.{Matcher});then()method on an unannotated inner class nested inside an annotated outer class(regression test added — see below).
All pre-existing exception paths in the rule (Yup/Joi, Promise/Deferred delegation, prototype
assignment, sibling then/catch/finally, JSON Schema conditionals, interface shape descriptors)
are unchanged.
Out of scope (tracked separately)
Gitar's automated "Implementation Status" check marks all 4 of JS-1821's "What" bullets as
covered here. Two of those are not actually addressed by this PR and are tracked
separately, so reviewers shouldn't read this PR as closing them:
@implements/implementscontract exception described above. Broader Deferred/Promiserecognition (e.g.
ns.Deferred = function () { this.then = ...; }, a MemberExpressionassignment target rather than a bare declaration name) is a narrow, separate gap in the
pre-existing naming-based exception, already tracked in
JS-2382 and fixed in a separate,
narrowly-scoped follow-up: JS-2408 Fix FN on S7739: recognize Deferred/Promise assigned via MemberExpression #7894.
Main, so test files aren'tanalyzed by this rule in the first place (consistent with JS-1397, which was cancelled for
the same reason).
Note on the community-proposed fix
The proposed fix attached to the Jira resolved the containing class via
context.sourceCode.getAncestors(node)(outermost-first) +.find(), which picks theoutermost matching class ancestor instead of the nearest one — flagged as a correctness
bug by the attached proposal review (
request_changes). This PR instead reuses the repo'sexisting
getAncestorsWithParent(node)helper (innermost-first) and adds a dedicatedOuter/Innernested-class regression test to lock in the corrected resolution.Tests
packages/analysis/src/jsts/rules/S7739/no-validation-lib/unit.test.ts— new valid cases(JSDoc
IThenable/Thenable, exported class, nested annotated/unannotated class) and newinvalid cases (no annotation, unrelated
@implements, nested-class regression, plain TSclass), plus a new TS
PromiseLikesuite viaNoTypeCheckingRuleTester.Baselines (Peach, before implementation — attached to JS-1821)
JS-1821-javascript-S7739-peach-before-20260903T081520Z.csv(216 issues)JS-1821-typescript-S7739-peach-before-20260903T081520Z.csv(486 issues)RSPEC
Companion RSPEC PR: SonarSource/rspec#8135 documents this exception in the rule
description (
Exceptionssection), with JS and TS examples matching this PR'simplementation and a caveat that the exception does not extend to
static thenmembers.