Skip to content
This repository was archived by the owner on Oct 3, 2024. It is now read-only.

Commit afef3fc

Browse files
no-extra-arguments: provide secondary issue locations (#97)
1 parent 2bca2e4 commit afef3fc

File tree

3 files changed

+133
-9
lines changed

3 files changed

+133
-9
lines changed

src/rules/no-extra-arguments.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ import {
2828
isIdentifier,
2929
isBlockStatement,
3030
} from "../utils/nodes";
31+
import { report, issueLocation, getMainFunctionTokenLocation, IssueLocation } from "../utils/locations";
3132

3233
const rule: Rule.RuleModule = {
34+
meta: {
35+
schema: [
36+
{
37+
// internal parameter
38+
enum: ["sonar-runtime"],
39+
},
40+
],
41+
},
3342
create(context: Rule.RuleContext) {
3443
const callExpressionsToCheck: Array<{
3544
callExpr: estree.SimpleCallExpression;
@@ -78,7 +87,7 @@ const rule: Rule.RuleModule = {
7887
"Program:exit"() {
7988
callExpressionsToCheck.forEach(({ callExpr, functionNode }) => {
8089
if (!usingArguments.has(functionNode) && !emptyFunctions.has(functionNode)) {
81-
report(callExpr, functionNode.params.length, callExpr.arguments.length);
90+
reportIssue(callExpr, functionNode);
8291
}
8392
});
8493
},
@@ -118,22 +127,52 @@ const rule: Rule.RuleModule = {
118127
}
119128
}
120129

121-
function report(callExpr: estree.SimpleCallExpression, expected: number, provided: number) {
130+
function reportIssue(callExpr: estree.SimpleCallExpression, functionNode: estree.Function) {
131+
const paramLength = functionNode.params.length;
132+
const argsLength = callExpr.arguments.length;
122133
// prettier-ignore
123134
const expectedArguments =
124-
expected === 0 ? "no arguments" :
125-
expected === 1 ? "1 argument" :
126-
`${expected} arguments`;
135+
paramLength === 0 ? "no arguments" :
136+
paramLength === 1 ? "1 argument" :
137+
`${paramLength} arguments`;
127138

128139
// prettier-ignore
129140
const providedArguments =
130-
provided === 0 ? "none was" :
131-
provided === 1 ? "1 was" :
132-
`${provided} were`;
141+
argsLength === 0 ? "none was" :
142+
argsLength === 1 ? "1 was" :
143+
`${argsLength} were`;
133144

134145
const message = `This function expects ${expectedArguments}, but ${providedArguments} provided.`;
135146

136-
context.report({ message, node: callExpr });
147+
report(
148+
context,
149+
{
150+
message,
151+
node: callExpr,
152+
},
153+
getSecondaryLocations(functionNode),
154+
);
155+
}
156+
157+
function getSecondaryLocations(functionNode: estree.Function) {
158+
const paramLength = functionNode.params.length;
159+
const secondaryLocations: IssueLocation[] = [];
160+
if (paramLength > 0) {
161+
const startLoc = functionNode.params[0].loc;
162+
const endLoc = functionNode.params[paramLength - 1].loc;
163+
// defensive check as `loc` property may be undefined according to
164+
// its type declaration
165+
if (startLoc && endLoc) {
166+
secondaryLocations.push(issueLocation(startLoc, endLoc, "Formal parameters"));
167+
}
168+
} else {
169+
// as we're not providing parent node, `getMainFunctionTokenLocation` may return `undefined`
170+
const fnToken = getMainFunctionTokenLocation(functionNode, undefined, context);
171+
if (fnToken) {
172+
secondaryLocations.push(issueLocation(fnToken, fnToken, "Formal parameters"));
173+
}
174+
}
175+
return secondaryLocations;
137176
}
138177
},
139178
};

src/utils/locations.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@
2020
import { Rule } from "eslint";
2121
import * as estree from "estree";
2222

23+
export interface IssueLocation {
24+
column: number;
25+
line: number;
26+
endColumn: number;
27+
endLine: number;
28+
message?: string;
29+
}
30+
31+
export interface EncodedMessage {
32+
message: string;
33+
cost?: number;
34+
secondaryLocations: IssueLocation[];
35+
}
36+
2337
/**
2438
* Returns a location of the "main" function token:
2539
* - function name for a function declaration, method or accessor
@@ -61,6 +75,46 @@ export function getMainFunctionTokenLocation(
6175
return location!;
6276
}
6377

78+
// As `ReportDescriptor` may contain either `message` or `messageId` prop,
79+
// we force the presence of `message` property by using the following type alias.
80+
export type ReportDescriptor = Rule.ReportDescriptor & { message: string };
81+
82+
/**
83+
* Wrapper for `context.report`, supporting secondary locations and cost.
84+
* Encode those extra information in the issue message when rule is executed
85+
* in Sonar* environment.
86+
*/
87+
export function report(
88+
context: Rule.RuleContext,
89+
reportDescriptor: ReportDescriptor,
90+
secondaryLocations: IssueLocation[] = [],
91+
cost?: number,
92+
) {
93+
const { message } = reportDescriptor;
94+
if (context.options[context.options.length - 1] === "sonar-runtime") {
95+
const encodedMessage: EncodedMessage = { secondaryLocations, message, cost };
96+
reportDescriptor.message = JSON.stringify(encodedMessage);
97+
}
98+
context.report(reportDescriptor);
99+
}
100+
101+
/**
102+
* Converts `SourceLocation` range into `IssueLocation`
103+
*/
104+
export function issueLocation(
105+
startLoc: estree.SourceLocation,
106+
endLoc: estree.SourceLocation = startLoc,
107+
message = "",
108+
): IssueLocation {
109+
return {
110+
line: startLoc.start.line,
111+
column: startLoc.start.column,
112+
endLine: endLoc.end.line,
113+
endColumn: endLoc.end.column,
114+
message,
115+
};
116+
}
117+
64118
function getTokenByValue(node: estree.Node, value: string, context: Rule.RuleContext) {
65119
return context
66120
.getSourceCode()

tests/rules/no-extra-arguments.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020
import { RuleTester } from "eslint";
21+
import { IssueLocation } from "../../src/utils/locations";
2122

2223
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
2324
import rule = require("../../src/rules/no-extra-arguments");
@@ -87,6 +88,18 @@ ruleTester.run("no-extra-arguments", rule, {
8788
message(2, 4, { line: 4, column: 9, endColumn: 24 }),
8889
],
8990
},
91+
{
92+
code: `
93+
function foo(p1, p2) {}
94+
// ^^^^^^>
95+
foo(1, 2, 3);
96+
//^^^^^^^^^^^^
97+
`,
98+
errors: [
99+
encodedMessage(2, 3, [{ line: 2, column: 21, endLine: 2, endColumn: 27, message: "Formal parameters" }]),
100+
],
101+
options: ["sonar-runtime"],
102+
},
90103
{
91104
code: `
92105
var foo = function() {
@@ -96,6 +109,20 @@ ruleTester.run("no-extra-arguments", rule, {
96109
`,
97110
errors: [message(0, 1, { line: 5, column: 9, endColumn: 15 })],
98111
},
112+
{
113+
code: `
114+
foo(1);
115+
//^^^^^^
116+
var foo = function() {
117+
// ^^^^^^^^>
118+
console.log('hello');
119+
}
120+
`,
121+
errors: [
122+
encodedMessage(0, 1, [{ line: 4, column: 18, endLine: 4, endColumn: 26, message: "Formal parameters" }]),
123+
],
124+
options: ["sonar-runtime"],
125+
},
99126
{
100127
code: `
101128
function foo(arguments) {
@@ -172,3 +199,7 @@ function message(
172199
...extra,
173200
};
174201
}
202+
203+
function encodedMessage(expected: number, provided: number, secondaryLocations: IssueLocation[]) {
204+
return JSON.stringify({ secondaryLocations, message: message(expected, provided).message });
205+
}

0 commit comments

Comments
 (0)