lint: add new deferunlockcheck pass#107577
Conversation
8e35304 to
7e11ceb
Compare
knz
left a comment
There was a problem hiding this comment.
could you also arrange for the check to be disabled if there's a special // nolint:XXX comment next to an Unlock call to say to disable the linter?
See the other linters - we have // nolint:errwrap, // nolint:errcmp etc.
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling)
7e11ceb to
4d41f2b
Compare
Santamaura
left a comment
There was a problem hiding this comment.
The check does account for this 😄 the exclusion rule is // nolint:deferunlock
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling)
rickystewart
left a comment
There was a problem hiding this comment.
Looks like CI is failing, have a look.
rickystewart
left a comment
There was a problem hiding this comment.
Oh, also, add //pkg/testutils/lint/passes/deferunlockcheck along with the other lint checks in the list in top-level BUILD.bazel as part of nogo.
4d41f2b to
fe62ff0
Compare
|
Looks like you will have to go into |
Santamaura
left a comment
There was a problem hiding this comment.
added
"deferunlockcheck": {
"only\_files": {
"cockroach/pkg/.\*$": "first-party code",
"cockroach/bazel-out/.\*/bin/pkg/.\*$": "first-party code"
}
},
to the nogo_config.json but build still seems to fail on external code..anything additional needed?
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling)
|
What's going on with the backslash? |
Santamaura
left a comment
There was a problem hiding this comment.
Odd it wasn't a typo, it's inserted when copy-pasting
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling)
|
You spelled the name of your linter wrong. It's called This diff works. Should you copy-paste it, be very careful you do not insert backslashes. |
|
It looks like you use |
|
Oh, and the PR calls it |
Santamaura
left a comment
There was a problem hiding this comment.
Yeah that was the problem thanks! In this scenario should I nolint the current offenders to pass CI?
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling)
If you do, at least file bugs so we can keep track of the current offenders. Otherwise they'll just get lost. |
fe62ff0 to
c13bf29
Compare
|
There is a group effort internally right now to address unsafe locks so I will hold off on merging this until that work progresses since it will resolve the problems this linter is picking up anyways. |
|
@rickystewart I have PRs that are addressing #107946 but there will still be many manual |
You want to |
0dc79ce to
3904acc
Compare
IMO a linter is exactly the right place to "start a conversation" between the reviewer and author about what needs to be sorted out. I don't think automatic PR comments are as effective at that. Seems like the major concern here would be resolved if we made sure there were fewer |
|
I'm also in favour of some form of linting for this pattern, thanks for working on it. Others already covered the reasons why I think it's important to have tooling to stop these kind of errors instead of relying on human reviews so I won't repeat that; at the end of the day, people are fallible and we will miss issues in the code when it's sufficiently complex. I also agree with the general sentiment that we could make the linter a little smarter. Things like:
make sense to me and would help us focus on the more likely to be problematic "unsafe" uses of
This shouldn't be too much work. We can keep track of which object is being locked and unlocked in a way that is similar to [1]. Line numbers can also be computed by calling [1] [2] https://pkg.go.dev/go/token#FileSet.Position |
| if !ok { | ||
| return | ||
| } | ||
| if sel.Sel != nil && (sel.Sel.Name == "Unlock" || sel.Sel.Name == "RUnlock") && !passesutil.HasNolintComment(pass, n, noLintName) { |
There was a problem hiding this comment.
We probably want to make sure we're only reporting issues with Unlock calls called on instances of sync{,util}.{RW}Mutex. Currently if a custom struct implements an Unlock function, the linter will complain about it and we probably don't want that, as the semantics are unknown to the linter.
Some similar code:
cockroach/pkg/testutils/lint/passes/loopvarcapture/loopvarcapture.go
Lines 404 to 407 in 75c6056
|
The linter will also currently complain about code like below, and it probably shouldn't: mu.Lock()
defer func() {
cancel()
mu.Unlock()
}() |
ba5eb1d to
1ce07ee
Compare
|
Made the "smarter" linter but still doing some testing. Need to update the |
fb21941 to
70f1b24
Compare
|
OK this should be RFAL |
|
Should also mention my team wants me to timebox any more time spent on this linter since it's not really obs inf domain so if this iteration ain't it then I'll probably need to hand it off to dev inf. |
stevendanna
left a comment
There was a problem hiding this comment.
Thanks for taking time to address the concerns around this linter.
For me, the inclusion of the "close enough" exception makes this something worth trying.
I'm not convinced we need to report an error on for/range statements. but can live with it if y'all feel it is important.
I briefly read over the linter code, but it should probably have some review from someone more familiar with the relevant APIs.
|
Not too sure who to get a review from maybe @renatolabs since you're familiar with the analyzer apis? |
| // shouldReportNonLinearControlFlow checks if locks has a *LockInfo without a matching unlock | ||
| // and if the node has a nolint comment near it. If it finds both it returns true. | ||
| func (lt *LockTracker) shouldReportNonLinearControlFlow(node ast.Node) bool { | ||
| hasNoLintComment := passesutil.HasNolintComment(lt.pass, node, noLintName) |
There was a problem hiding this comment.
Seems like you can immediately if hasNoLintComment { return false }, right? We will always return false in this case with the way this function is currently written.
| } | ||
|
|
||
| var ( | ||
| noLintName = "deferunlock" |
There was a problem hiding this comment.
Nit: noLintName and maxLineDistance should be const, not var.
| if lockDistance > maxLineDistance && !passesutil.HasNolintComment(lt.pass, node, noLintName) { | ||
| lt.issues = append(lt.issues, analysis.Diagnostic{ | ||
| Pos: unlockPos, | ||
| Message: fmt.Sprintf("{RW}Mutex {R}Unlock is >%d lines away from matching {R}Lock", maxLineDistance), |
There was a problem hiding this comment.
The error message here (and on line 311) is not great. Note that this is the message that will be displayed to devs when they violate the lint rule. In particular it's unclear what someone is supposed to do to solve the problem. e.g. "Oh, I can't have more than 5 lines between the lock and unlock, so I need to start combining multiple lines of code into one line with semicolons" or something like that.
The error message should succinctly describe what the actual problem is and suggest a fix like "move the Unlock() call to a defer statement immediately after the Lock() call". If you produce something the developer can directly copy-paste (like defer x.y.whatever.Unlock() -- I think you actually already have the pieces you need to make this work), even better.
The error message on line 311 has an additional problem because what is "non-linear control flow"? Why is it a problem if I have non-linear control flow, and how do I fix it?
This commit adds a new pass which checks for `...Unlock()` expressions without `defer` which could result in a lock being held indefinitely. Resolves cockroachdb#105366 Release note: None
for linter to pass This commit adds a number of files to the nogo config for the linter to ignore. These files have manual unlocks that are catagorized as potentially problematic since there are unlocks >10 lines away from the lock. The commit also applies the nolint rule to any remaining manual unlock cases that are <=10 lines away from the lock. Release note: None
This commit udpates the lock linter to work by keeping track of locks and if we find any if conditions, loops, or function calls before finding a matching unlock it will report. It will also report if the matching unlock is >5 lines away from the lock. It will ignore cases where a `nolint:deferunlock` is present. Release note: None
70f1b24 to
d80a9f5
Compare
Santamaura
left a comment
There was a problem hiding this comment.
Reviewable status:
complete! 0 of 0 LGTMs obtained (waiting on @ericharmeling, @renatolabs, and @rickystewart)
pkg/testutils/lint/passes/deferunlockcheck/deferunlockcheck.go line 40 at r13 (raw file):
Previously, rickystewart (Ricky Stewart) wrote…
Nit:
noLintNameandmaxLineDistanceshould beconst, notvar.
Done
pkg/testutils/lint/passes/deferunlockcheck/deferunlockcheck.go line 286 at r13 (raw file):
Previously, rickystewart (Ricky Stewart) wrote…
The error message here (and on line 311) is not great. Note that this is the message that will be displayed to devs when they violate the lint rule. In particular it's unclear what someone is supposed to do to solve the problem. e.g. "Oh, I can't have more than 5 lines between the lock and unlock, so I need to start combining multiple lines of code into one line with semicolons" or something like that.
The error message should succinctly describe what the actual problem is and suggest a fix like "move the
Unlock()call to adeferstatement immediately after theLock()call". If you produce something the developer can directly copy-paste (likedefer x.y.whatever.Unlock()-- I think you actually already have the pieces you need to make this work), even better.The error message on line 311 has an additional problem because what is "non-linear control flow"? Why is it a problem if I have non-linear control flow, and how do I fix it?
Thanks for the feedback, I made the error messages a bit simpler and added what the dev can do to fix it.
pkg/testutils/lint/passes/deferunlockcheck/deferunlockcheck.go line 298 at r13 (raw file):
Previously, rickystewart (Ricky Stewart) wrote…
Seems like you can immediately
if hasNoLintComment { return false }, right? We will always return false in this case with the way this function is currently written.
Agree, done.
|
TFTRs! bors r+ |
|
Build succeeded: |

This commit adds a new pass which checks for
...Unlock()expressions withoutdeferwhichcould result in a lock being held indefinitely.
Resolves #105366
Release note: None