Skip to content

Commit 83d1074

Browse files
sonar-nigel[bot]Vibe Botclaudegithub-actions[bot]francois-mora-sonarsource
authored
JS-1423 Fix S7778 false positives for custom class methods with single argument (#6555)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Francois Mora <francois.mora@sonarsource.com>
1 parent cba6306 commit 83d1074

6 files changed

Lines changed: 422 additions & 7 deletions

File tree

its/ruling/src/test/expected/eigen/typescript-S7778.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
2-
"eigen:src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx": [
3-
366
4-
],
52
"eigen:src/app/Scenes/Artwork/Artwork.tsx": [
63
243
74
],

packages/analysis/src/jsts/rules/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,6 @@ SonarJS uses some rules are not shipped in this ESLint plugin to avoid duplicati
564564
| S7775 | [unicorn/prefer-regexp-test](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-regexp-test.md) |
565565
| S7776 | [unicorn/prefer-set-has](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-set-has.md) |
566566
| S7777 | [unicorn/prefer-set-size](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-set-size.md) |
567-
| S7778 | [unicorn/prefer-single-call](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-single-call.md) |
568567
| S7780 | [unicorn/prefer-string-raw](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-string-raw.md) |
569568
| S7781 | [unicorn/prefer-string-replace-all](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-string-replace-all.md) |
570569
| S7783 | [unicorn/prefer-string-trim-start-end](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-string-trim-start-end.md) |
@@ -660,6 +659,7 @@ The following rules are used in SonarJS but not available in this ESLint plugin.
660659
| S7759 | [unicorn/prefer-date-now](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-date-now.md) |
661660
| S7763 | [unicorn/prefer-export-from](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-export-from.md) |
662661
| S7770 | [unicorn/prefer-native-coercion-functions](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-native-coercion-functions.md) |
662+
| S7778 | [unicorn/prefer-single-call](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-single-call.md) |
663663
| S7784 | [unicorn/prefer-structured-clone](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-structured-clone.md) |
664664
| S7785 | [unicorn/prefer-top-level-await](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-top-level-await.md) |
665665

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
import type { Rule } from 'eslint';
18+
import type estree from 'estree';
19+
import type { TSESTree } from '@typescript-eslint/utils';
20+
import ts from 'typescript';
21+
import { generateMeta } from '../helpers/generate-meta.js';
22+
import { interceptReport } from '../helpers/decorators/interceptor.js';
23+
import {
24+
isRequiredParserServices,
25+
type RequiredParserServices,
26+
} from '../helpers/parser-services.js';
27+
import { getTypeFromTreeNode } from '../helpers/type.js';
28+
import * as meta from './generated-meta.js';
29+
30+
export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
31+
return interceptReport(
32+
{
33+
...rule,
34+
meta: generateMeta(meta, rule.meta),
35+
},
36+
reportExempting,
37+
);
38+
}
39+
40+
function reportExempting(context: Rule.RuleContext, descriptor: Rule.ReportDescriptor) {
41+
if (!('node' in descriptor)) {
42+
context.report(descriptor);
43+
return;
44+
}
45+
46+
const services = context.sourceCode.parserServices;
47+
if (!isRequiredParserServices(services)) {
48+
// No TypeScript type information available; pass through unchanged (conservative fallback)
49+
context.report(descriptor);
50+
return;
51+
}
52+
53+
const { node } = descriptor;
54+
const tsNode = node as TSESTree.Node;
55+
const parent = tsNode.parent;
56+
57+
// Determine the callee node whose call signatures we'll inspect:
58+
// - method call (push, add, remove): the MemberExpression is the callee
59+
// - direct function call (importScripts): the reported Identifier is the callee
60+
let callee: TSESTree.Node;
61+
if (parent?.type === 'MemberExpression') {
62+
callee = parent;
63+
} else if (parent?.type === 'CallExpression') {
64+
callee = tsNode;
65+
} else {
66+
context.report(descriptor);
67+
return;
68+
}
69+
70+
if (calleeAcceptsMultipleArguments(callee, services)) {
71+
context.report(descriptor);
72+
}
73+
}
74+
75+
/**
76+
* Returns true if the callee can accept more than one argument.
77+
* When signatures are unresolved (empty), returns true as a conservative fallback
78+
* to avoid false negatives. Single-argument callees with resolved signatures are
79+
* suppressed as false positives.
80+
*/
81+
function calleeAcceptsMultipleArguments(
82+
callee: TSESTree.Node,
83+
services: RequiredParserServices,
84+
): boolean {
85+
const calleeType = getTypeFromTreeNode(callee as unknown as estree.Node, services);
86+
const signatures = calleeType.getCallSignatures();
87+
// If signatures cannot be resolved, fall back to reporting (conservative behavior)
88+
if (signatures.length === 0) {
89+
return true;
90+
}
91+
return signatures.some(sig => {
92+
const params = sig.parameters;
93+
const lastParam = params.at(-1);
94+
if (lastParam === undefined) {
95+
return false;
96+
}
97+
const decl = lastParam.valueDeclaration;
98+
return (
99+
(decl !== undefined && ts.isParameter(decl) && !!decl.dotDotDotToken) || params.length > 1
100+
);
101+
});
102+
}

packages/analysis/src/jsts/rules/S7778/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
import { rules } from '../external/unicorn.js';
18-
export const rule = rules['prefer-single-call'];
18+
import { decorate } from './decorator.js';
19+
20+
export const rule = decorate(rules['prefer-single-call']);

packages/analysis/src/jsts/rules/S7778/meta.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
// https://sonarsource.github.io/rspec/#/rspec/S7778/javascript
18-
export const implementation = 'external';
18+
export const implementation = 'decorated';
1919
export const eslintId = 'prefer-single-call';
20-
export const externalPlugin = 'unicorn';
20+
export const externalRules = [{ externalPlugin: 'unicorn', externalRule: 'prefer-single-call' }];
2121
export const quickFixMessage = 'Combine into single call';

0 commit comments

Comments
 (0)