Sonarjs/no-extra-argument eslint rule doesn’t recognize change of function definition

  • What language is this for? JavaScript
  • Which rule? sonarjs/no-extra-arguments (from eslint-plugin-sonarjs)
  • Why do you believe it’s a false-positive/false-negative? executing the code works fine but the rule reports a bug
  • How can we reproduce the problem? Give us a self-contained snippet of code (formatted text, no screenshots)

The bug can be reproduced in this dedicated repository: GitHub - DamienCassou/sonarjs-issue-no-extra-argument: Reproduce an isuse with sonarjs.

The problematic code is:

function main() {
  let update = makeUpdate();

  update("hello");
}

function makeUpdate() {
	let updateFn = () => true;

	fetchItems({
		setUpdateFn: (fn) => (updateFn = fn),
	});

	return (...args) => updateFn(...args);
}

function fetchItems({setUpdateFn}) {
  setUpdateFn((arg) => console.log(arg));
}

main()

eslint reports an issue on the return line:

  14:22  error  This function expects no arguments, but 1 was provided  sonarjs/no-extra-arguments

✖ 1 problem (1 error, 0 warnings)

The rule is convinced that updateFn() takes no argument (probably because of its definition in the first line of makeUpdate()), ignoring that the function can be redefined.

@DamienCassou, thanks for the bug report. The issue that you raised is very interesting.

The first thing that comes to my mind is that the rule is actually detecting something not adaptable in your code: as soon as we pass a function that accepts more than one parameter to setUpdateFn, the code would not behave as expected:

function main() {
    let update = makeUpdate();

    update("hello");
}

function makeUpdate() {
    let updateFn = () => true;

    fetchItems({
        setUpdateFn: (fn) => (updateFn = fn),
    });

    return (...args) => updateFn(...args);
}

function fetchItems({setUpdateFn}) {
    setUpdateFn((arg, others) => console.log(arg, others));
}

main()

When we execute this script, we see that hello undefined is logged to the console, which is expected since main always execute update() by passing it one single parameter, and we can’t do anything about that from outside main. Said differently, there is no point at passing to setUpdateFn a function that accepts less or more than one single parameter.

What this means is that updateFn is a function that accepts one parameter and that returns any, and the first assignment to it is not reflecting that: let updateFn = () => true;

I recommend that you update the first assignment to let updateFn = (arg) => true;. This would make the error disappear by explictely establishing the signature of updateFn.

Now, allow me to go a bit further: for no-extra-arguments to actually behave as you expect, it would require some code interpretation.

Let’s consider this code sample:

const main = (numberOfArguments, value) => {
    let updateFn = () => {
        console.log('No argument passed');
    };
    
    let updateFnWasChanged = false;

    if (numberOfArguments) {
        updateFn = (arg) => {
            console.log(arg);
        };

        updateFnWasChanged = true;
    }

    if (updateFnWasChanged) {
        updateFn(value);
    }
    else {
        updateFn();
    }
}

main(1, 'hello');

Today, the rule would raise at line 13. But in a perfect world, it would not raise at all because, if the execution flow enters in the if (updateFnWasChanged) { branch, it is guaranteed that updateFn is a function that actually accepts one argument. We know that by reading the code, but for a rule to actually understand that, it would need to interpret the code and keep track of the fact that if updateFnWasChanged is true, then updateFn is actually accepting one parameter.

Unfortunately, this is outside the current scope of the rule, which can only statically analyze the code. But this is a very exciting route to explore in the future.

Anyway, sorry for the long post. Let me know what you think.

Cheers,

Eric.

Thank you very much for your answer. It makes complete sense. I definitely understand that static analysis cannot understand the meaning of my code. As a result, I think there are only 2 meaningful options the rule could follow: either enforce a number of parameters OR give up because static analysis isn’t good enough.

Option 1: Enforce a number of parameters

In the first option, the rule decides that the function takes 0 parameters because its default value () => true does. The rule should then report any call/definition with parameters. I think this makes perfect sense because the code is clearer if all calls/definitions have the same number of parameters. This option has the advantage of flagging more code at the expense of reporting more false positives.

In this option, I think the rule should flag this code:

function makeUpdate() {
	let updateFn = () => true;

	fetchItems({
		setUpdateFn: (fn) => (updateFn = (foo) => fn(foo)),
	});

	return updateFn
}

The code assigns updateFn to a function of 1 parameter but the function is supposed to have none. Unfortunately, no-extra-argument doesn’t detect this problem. Is it because no-extra-argument focuses on the call site and not the definition site? Could it be the responsibility of another rule to detect that the overrides of a function should have the same number of parameters?

Option 2: Give up because static analysis isn’t good enough

There is another option though. The rule could detect that it cannot decide statically and should then give up. In this case, I would expect the rule to leave me alone because it sees that updateFn can be overridden in

fetchItems({
  setUpdateFn: (fn) => (updateFn = fn),
});

This option has the advantage of reducing false positives as it prefers not reporting potential issues.

Conclusion

I believe that in both options, Sonarjs could be improved. That being said, Sonarjs is already awesome so I’m not complaining :-).

2 Likes

@DamienCassou

I love the first option, and it could be part of a more general rule about assignments like the Python S5890 one. Granted, the Typescript checker already does that, but such a rule would be very lovely for JavaScript, more so when JSDoc annotations are used.

I created a ticket to track this suggestion: [JS-377] - Jira

Thanks a lot for your time, feedback, and ideas. This is much appreciated.

Eric.

1 Like