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

Incorrectly identifies channel draining as "empty code block" #386

Open
jpopadak opened this issue Apr 9, 2020 · 19 comments · Fixed by #415 · May be fixed by #818
Open

Incorrectly identifies channel draining as "empty code block" #386

jpopadak opened this issue Apr 9, 2020 · 19 comments · Fixed by #415 · May be fixed by #818
Assignees
Labels

Comments

@jpopadak
Copy link

jpopadak commented Apr 9, 2020

Describe the bug
revive incorrectly identities channel draining as an empty code block and suggests to remove it.

To Reproduce
Steps to reproduce the behavior:

  1. I updated revive go get -u github.com/mgechev/revive
  2. I run it with the following flags & configuration file:
cd $PROJECT_DIR
revive -config revive.toml .
ignoreGeneratedHeader = false
severity = "error"
confidence = 0.8
errorCode = 1
warningCode = 0

[rule.add-constant]
    arguments = [{maxLitCount = "3",allowStrs ="\"\"",allowInts="0,1,2"}]
[rule.argument-limit]
    arguments = [6]
[rule.atomic]
[rule.blank-imports]
[rule.bool-literal-in-expr]
[rule.call-to-gc]
[rule.constant-logical-expr]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.cyclomatic]
    arguments = [10]
[rule.deep-exit]
[rule.dot-imports]
[rule.duplicated-imports]
[rule.empty-block]
[rule.error-naming]
[rule.error-return]
[rule.error-strings]
[rule.errorf]
[rule.exported]
[rule.flag-parameter]
[rule.function-result-limit]
    arguments = [4]
[rule.get-return]
[rule.if-return]
[rule.import-shadowing]
[rule.increment-decrement]
[rule.indent-error-flow]
[rule.line-length-limit]
    arguments = [120]
[rule.package-comments]
[rule.range]
[rule.range-val-in-closure]
[rule.receiver-naming]
[rule.redefines-builtin-id]
[rule.superfluous-else]
[rule.time-naming]
[rule.unexported-return]
[rule.unhandled-error]
[rule.unreachable-code]
[rule.unused-parameter]
[rule.unused-receiver]
[rule.var-declaration]
[rule.var-naming]
[rule.waitgroup-by-value]

A really bad example, but in our scenario, we have many routines reading from processChan where we send another message to have them stop processing. We reuse the channel, but we want to ensure the channel was empty from previous processing.

package main

import "sync"

const (
	chanSize = 10
	maxNums = 100
	modNum = 5
)

func main() {
	wg := sync.WaitGroup{}
	processChan := make(chan int, chanSize)

	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < maxNums; i++ {
			processChan <- i
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()

		for i := 0; i < maxNums; i++ {
			if !isValidNum(i) {
				break
			}
		}
	}()

	for range processChan {
	}
}

func isValidNum(i int) bool {
	return i%modNum == 0
}
  1. Revive returns an error

myflie.go:34:24: this block is empty, you can remove it

Expected behavior
No error to occur as this is channel draining.

Logs
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • OS: macOS 10.14.6
  • Version of Go: v1.14.1

Additional context
We have two workarounds:

  1. We can ignore the few lines across our project where we are iterating over a channel, however, it would be nicer if revive knew that this is channel draining.
  2. We can delete and recreate the channel, but since the channel is passed around on startup / Dependency Injection, we cannot just "create a new one and reinitialize all services" easily.
@mgechev
Copy link
Owner

mgechev commented Apr 10, 2020

Looks like we should type check to verify that we're dealing with a channel. This would slow down the rule though, but seems like the right way to go.

@chavacava
Copy link
Collaborator

For the sake of rule simplicity, I think these corner cases should be handled by disabling the linter in the concerned lines.
Anyway, I've pushed PR #415 with a fix based in type information.

@mgechev
Copy link
Owner

mgechev commented May 20, 2020

@jpopadak, because of some regressions (both stability and performance), we reverted the fix. It's still possible to recognize this pattern without type checking. This is something we'd investigate in the future.

@powerman
Copy link

powerman commented Jul 7, 2021

Would be nice to have this fixed. I've a project with dozen of such false positives and have to disable the rule. :(

@chavacava
Copy link
Collaborator

branch https://github.com/mgechev/revive/tree/fix-386 implements a fix consisting in trying to identify channel draining loops by their syntax (a range without value nor key) and avoids raising a failure for such cases

branch https://github.com/mgechev/revive/tree/alleviates-386 also identifies channel draining loops by their syntax but in place of not raising a failure it raises one with a lower (0.5) confidence.

Neither implementation uses type information

@feldgendler
Copy link

The fix for this bug was reverted soon after being introduced. Are the reasons in #416 still valid? That was in 2020, and some things might have changed since (for instance, Go modules are now the norm).

I can see that many other revive checks use type information. Can't this one?

@chavacava
Copy link
Collaborator

Hi @feldgendler, thanks for commenting in this thread.
Rules requiring type information introduce some performance degradation thus we try to keep rules type free
At the time we decided to not evolve the rule because in the trade-off analysis precision vs performance degradation+complexity we considered the number of false positives not high enough to justify the use of type information to avoid them (and adding a disabling annotation in the rare cases was not too much effort)
It is something we can re-open to discussion. I'll try to rebase the branches I've pushed with the fix proposals long time ago.

@feldgendler
Copy link

Hi! Thanks for coming back to me @chavacava!

Do I understand correctly that if any revive rule that needs type information is enabled, that triggers type analysis, and once it's triggered, any such rules have negligible marginal cost?

Also, is it true that all revive rules that currently require type information are disabled by default?

@chavacava
Copy link
Collaborator

Yes, and yes

@feldgendler
Copy link

Would it be possible to add a fine-tuning option to the empty-block rule (whether to use type information) so that one could choose between accuracy and performance?

@chavacava
Copy link
Collaborator

I've rebased the branch #415 and now the rule does not warn on empty blocks on channel draining ranges.
@feldgendler you can try the fix and let me know if it is sufficiently precise for you.

@feldgendler
Copy link

@chavacava Yes, thank you! My code doesn't drigger empty-block with this fix.

@twpayne
Copy link

twpayne commented Oct 5, 2024

For info, I believe I've hit this same issue with the following code:

https://github.com/twpayne/go-heap/blob/fb16bd2df012f1689aa9c0682b0c611078c10908/heap.go#L39-L40

revive incorrectly reports that the block can be removed:

heap.go:39:69: empty-block: this block is empty, you can remove it (revive)
                for value, ok := h.Pop(); ok && yield(value); value, ok = h.Pop() {
                }

This is with revive run by golangci-lint 1.61.0, i.e. revive v1.3.9.

@chavacava
Copy link
Collaborator

Hi @twpayne thanks for reporting the issue.
Effectively, the rule fails to detect the side-effect of the loop predicate. Doing that kind of analysis in a complete way is beyond the capabilities of revive (and of any static analysis tool in the general case)
As an immediate "fix" I can just propose to deactivate the rule at that line.
I'm studying to modify the rule to just skip analyzing loops to avoid false positives.

@twpayne
Copy link

twpayne commented Oct 8, 2024

Thank you very much for the analysis and explanation. I've indeed added a //nolint:revive comment to suppress the warning for that particular line.

I agree that any complete static analysis is infeasible. Maybe a couple of heuristics like "does any part of the for clause, condition, or range clause contain a function call or an assignment to a variable outside the loop" might help reduce the false positives of this rule. Just a thought, I don't know how easy this would be to implement or how well it work in practice.

@feldgendler
Copy link

Thank you very much for the analysis and explanation. I've indeed added a //nolint:revive comment to suppress the warning for that particular line.

I agree that any complete static analysis is infeasible. Maybe a couple of heuristics like "does any part of the for clause, condition, or range clause contain a function call or an assignment to a variable outside the loop" might help reduce the false positives of this rule. Just a thought, I don't know how easy this would be to implement or how well it work in practice.

Or ++/--, or a channel op.

@chavacava
Copy link
Collaborator

@twpayne the rule already have a very primitive version of such an heuristic but as you mention (and @feldgendler comment confirms) the cases are "infinite".
IMO the true positives this rule is spotting are very very few vs the false positives, thus I think we should just give up and do not introduce complexity to catch a negligible number of true loops with empty blocks.

@twpayne
Copy link

twpayne commented Oct 8, 2024

IMO the true positives this rule is spotting are very very few vs the false positives, thus I think we should just give up and do not introduce complexity to catch a negligible number of true loops with empty blocks.

Ack, makes sense. Is it worth closing this issue as "not planned" or "won't fix"?

@feldgendler
Copy link

If I had a say, I would err for the side of false negatives rather than false positives. In this context, it means that an empty loop body is not reported as “empty code block” — either never reported at all, or only reported if the loop statement itself is definitely side-effect-free (that is, if in doubt, don't report).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
6 participants