Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create rule S6959: "Array.reduce()" calls should include an initial value #4626

Merged
merged 5 commits into from Mar 27, 2024

Conversation

yassin-kammoun-sonarsource
Copy link
Contributor

Fixes #4591

Copy link
Contributor

@zglicz zglicz left a comment

Choose a reason for hiding this comment

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

Please add an invalid test case before merging

code: 'xs.reduce((acc, x) => acc + x);',
},
],
invalid: [],
Copy link
Contributor

Choose a reason for hiding this comment

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

could you add an invalid case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you come back to the rule implementation, you might have noticed this weird check:

const services = context.parserServices;
if (!isRequiredParserServices(services)) {
return {};
}

While the function isRequiredParserServices isn't well-named, it actually tests that type-checking is available. If not, the rule turns into a noop. In order to test this behavior, this file uses ESLint rule tester for code coverage purposes. We currently don't have any other way to test this edge case (or at least, we haven't invested time on a better solution so far). That's why there is intentionally no invalid test case here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm reading and re-reading and I don't understand.
In this test, is there type-checking available?
I assume no, and thanks to that this rule becomes a no-op, as the code sample that is provided is missing the initial value.

Why isn't there typechecking in this test then? What should be the parserOptions to include type-checking? Or am I completely off-base here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When we parse JS/TS code, we rely on TypeScript ESLint parser which is able to parse both syntaxes. In addition, TypeScript ESLint wraps some utilities from TypeScript compiler, including type-checking. However, to benefit from type-checking, you need to configure the parsing options with a TSConfig somewhere.

By default, ESLint's rule tester uses Esprima (I think) to parse the code being tested. If you want to have type-checking available while using this rule tester, you first need to manually configure the parser to use as follows:

const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: { ecmaVersion: 2018 },
});

However, this is not enough as it only allows us to parse TypeScript syntax as well. To enable type-checking, you need to tweak ESLint's rule tester with a TSConfig. I am not going to show how because we want to avoid that and prefer using the comment-based testing framework which always makes type-checking available.

I'm reading and re-reading and I don't understand. In this test, is there type-checking available?

No, type-checking is not available because we use ESLint's rule tester without customization.

I assume no, and thanks to that this rule becomes a no-op, as the code sample that is provided is missing the initial value.

Exactly!

Why isn't there typechecking in this test then? What should be the parserOptions to include type-checking? Or am I completely off-base here?

  1. Configure the parser options to use TypeScript ESLint parser
  2. Configure ESLint's rule tester with a TSConfig

return xs.reduce((acc, x) => acc + x, 0); // Compliant
}

function coverage(x: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file as a whole is not executed at all but rather denotes a test file with actual unit tests. The file uses a homemade testing framework that we call comment-based testing framework. A test file contains special comments indicating that a rule should raise an issue at specific locations. For instance, this is what is meant here:

function noncompliant(xs: number[]) {
return xs.reduce((acc, x) => acc + x); // Noncompliant {{Add an initial value to this "reduce()" call.}}
}

The comment // Noncompliant indicates that an issue should be raised at line 2 and whatever is inside {{...}} is the expected message of the reported issue. We could assert even more things like code fixes, starting and ending columns, and secondary locations.

Coming back to your question, I guess you should understand by now what's the purpose of the function coverage. No issue will be raised here, the rule will visit those method invocations for the sake of increasing the code coverage of the rule implementation, this line in other words:

if (isCallingMethod(node, 1, 'reduce') && isArray(node.callee.object, services)) {

Copy link
Contributor

Choose a reason for hiding this comment

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

So this is just to increase code-coverage? That seems counter-productive and actually adds confusion where it shouldn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't necessarily agree that's confusing since the test file explicitly separates what's noncompliant from what's compliant and what's for coverage. Maybe I am missing something to make it less confusion.

Feel free to suggest anything that comes to your mind :)

"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
Copy link
Contributor

Choose a reason for hiding this comment

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

heh, that's generous if not intimidating

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! Could you actually comment on this PR?

Each rule has a description, and a rule description is stored in the RSPEC GitHub repository, a company-wide database for all Sonar rule descriptions. New descriptions or description updates are first introduced in this repository through pull requests and eventually merged. To bring back those changes in a given analyzer, we then use an internal tool called rule-api that fetches any changes for any rule of the analyzer or changes for a specific rule.

You will get familiar with this tool tomorrow when starting the release process of the analyzer.

Copy link
Contributor

@zglicz zglicz left a comment

Choose a reason for hiding this comment

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

Thank you for the explanations. It's now making a bit more sense, but still I left a few questions.

return xs.reduce((acc, x) => acc + x, 0); // Compliant
}

function coverage(x: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So this is just to increase code-coverage? That seems counter-productive and actually adds confusion where it shouldn't.

code: 'xs.reduce((acc, x) => acc + x);',
},
],
invalid: [],
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm reading and re-reading and I don't understand.
In this test, is there type-checking available?
I assume no, and thanks to that this rule becomes a no-op, as the code sample that is provided is missing the initial value.

Why isn't there typechecking in this test then? What should be the parserOptions to include type-checking? Or am I completely off-base here?

Comment on lines +41 to +47
function isArray(node: estree.Node) {
if (isRequiredParserServices(services)) {
return isArrayType(node, services);
} else {
return isArrayExpression(getUniqueWriteUsageOrNode(context, node));
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zglicz I updated the array detection so that it tries to determine syntactically if a node denotes an array in case type-checking is missing for whatever reason.

@yassin-kammoun-sonarsource yassin-kammoun-sonarsource merged commit 06615d0 into master Mar 27, 2024
14 checks passed
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.

Create rule S6959: "Array.reduce()" calls should include an initial value
2 participants