-
Notifications
You must be signed in to change notification settings - Fork 286
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
modifies-value-receiver: add support for immutable values #1066
Comments
To me it looks like a design flaw in the given code. |
It does that - by using non-pointer receiver.
Function will do the same (receive arg of non-pointer Config type and thus gets a copy), but it'll have longer name like I agree it may looks a bit unusual, maybe even confusing - but this will gone as soon as you get used to this style. As for "design flaw" or "indeed suspicious" - I don't see how these apply. Can you explain what exactly the design flaw is and what kind of suspicions this code raises? |
Answering to all of your comments at once: this code style lacks readability and causes confusion. I'd avoid having it. It's still your and your teammates' choice, but I don't think we should change |
Hi @powerman, thank you for the proposal. |
TBH I don't see much use for this pattern except for Config-like structures. But Config structures are wide spread, so IMO it's worth support. For Config structures it's important to have both required and optional args, and ensure no references to config will exists outside after calling a New constructor (to make sure object configuration can't be modified at any moment from outside). |
Here are some examples of the pattern being used to other things than configurations (notice the cases where the receiver copy is not returned but used, after modification, to build a struct of a different type) kratos/identity/identity.go:334:2: suspicious assignment to a by-value method receiver |
While this is not an error by itself, I doubt this case worth supporting:
|
Hi @powerman, I've drafted a modification of the rule to do not warn on cases where the intent is to modify the receiver in order to return it. At the moment, the rule adds a The branch with the modified version is here, feedback is welcome. cc: @denisvmedia |
Works for me. |
@chavacava maybe we can have a different confidence value for the "(false positive?)" case. WDYT? |
What about adding the struct name to the error reported when false-positive? This way they could be excluded by using golangci-lint when a well identified struct could be ignored https://golangci-lint.run/usage/false-positives/#exclude-or-skip |
@denisvmedia @ccoVeille It looks like you both missed the "The idea is to remove these failures in the final version of the rule." part of @chavacava message. I read it this way: there will be no |
Thanks for your reply @powerman I was also unsure how do read the message. It was a bit ambiguous to me. I chose one version, you chose yours. I might be likely to have chosen the wrong one 😅 Let's assume it's your version: The linter evolves in the way of no longer reporting what could be considered as false-positive. Then I'm a bit worried there might be some rare legitimate use cases of someone altering a non-pointer receiver, while it should be a pointer one, so something that might be a bug won't be reported. Something like, returning a struct from struct, but updating a counter in the struct. It's uneasy to know if such use cases exists, because if such use cases happened in public code using revive, it would have been already reported, and fixed, I think. If the changes goes I'm the way you said, these bugs would be never reported in someone code via the linting, so never fixed maybe. Let's wait for @chavacava feedback. |
Can you show an example of such a code, please? To avoid some more misunderstanding. 😄 |
Not really, as I said it's hypothetical, but something like this maybe type Logger struct {
// whatever
Counter int
}
func (l Logger) Foo() Logger {
l.Counter++
return l
} |
Maybe @alingse could help us, by running go-linter-runner by running revive with this exact version 92c3209 and launch the |
Hypothetical is okay, but why do you think this code is wrong? To me this example looks exactly like ones in the first comment:
Any chance you can craft a (hypothetical, of course) example which looks like an obvious bug? At a glance such example probably should:
I can't say it's impossible to craft such an example, but chances are this code will looks ugly in one way or another and this unliness might be catched by some other linter. BTW, I just notice current (unmodified) version of modifies-value-receiver rule does NOT catch |
Good catch, I think so EDIT it was created |
Yes, maybe Let's hope that what alingse launched would help to identity if it exists |
Hi, sorry for the late response (I was offline the last 48h) @denisvmedia about using different confidence level. It was something I've considered but a) I suspect almost nobody configures revive's confidence level, b) the failure itself says "Suspicious ..." thus the confidence is already not 100%. As I've said before, I propose to do not report all those failures that today are marked with I think that the bigger difficulty with this rule is that in some cases we need to understand the intent of the function to decide if it is or not an acceptable modification of a copy of the receiver. For example, in the example given by @ccoVeille type Logger struct {
// whatever
Counter int
}
func (l Logger) Foo() Logger {
l.Counter++
return l
} I will say that it depends on the meaning of Of course, |
@chavacava Thanks for your reply You are right about the naming it's what makes the difference for the same code. I'm glad you analyzed big codebase. Let's say it's better to have less false-positive, even there are a very rare case of badly designed code that might contain a bug. So I'm fine with the PR |
One question, what would be the behavior of the linter with a method that return a pointer Something like func (l Logger) Foo() *Logger {
l.Counter += 1
return &l
} Same question with a method that return a signature with error, quite common func (l Logger) Foo() (Logger, err) {
l.Counter += 1
return l, nil
} |
BTW what do you think about the repository to automate detection like the one you do manually now |
For anyone interested, @powerman @chavacava Here are the results via @alingse tool |
Here is a result of what is currently detected with revive 1.5.0, but that will no longer be reported with the version "false-positive?" one. https://github.com/firecracker-microvm/firecracker-go-sdk/blob/main/jailer.go#L176-L198 // WithBin will set the specific bin path to the builder.
func (b JailerCommandBuilder) WithBin(bin string) JailerCommandBuilder {
b.bin = bin
return b
}
// WithID will set the specified id to the builder.
func (b JailerCommandBuilder) WithID(id string) JailerCommandBuilder {
b.id = id
return b
}
// WithUID will set the specified uid to the builder.
func (b JailerCommandBuilder) WithUID(uid int) JailerCommandBuilder {
b.uid = uid
return b
}
// WithGID will set the specified gid to the builder.
func (b JailerCommandBuilder) WithGID(gid int) JailerCommandBuilder {
b.gid = gid
return b
} Here the code is creating a copy via the non-pointer receiver. It's a way to keep the struct immutable Then we have to discuss if it's a problem or not. Previously, it was reported Originally posted with @alingse help in alingse/go-linter-runner-example#1 (comment) Run Got total 26 lines output in action: https://github.com/alingse/go-linter-runner-example/actions/runs/11799381973 Expand
Report issue: https://github.com/firecracker-microvm/firecracker-go-sdk/issues |
Can you please explain why do you think we have to discuss this code? At a glance I don't see how it differs from my use case in the beginning of this issue. Builder pattern is supposed to be used in exactly this way ( |
The more examples I look in the reporting, the more I'm convinced it's a good pattern. That should indeed not being reported. You use a copy, so you return a new object, then the function keeps the struct immutable. It's a good thing. There might be very rare use cases where something like this won't be doing what it expected. func (a A) Foo() A {
a.whatever = true
return a
} But let's say we don't care. It's better to have less false-positive So yes, I agree, we should avoid reporting them Sorry for the discussion, but talk is cheap, missing bugs is not. That's why I wanted @alingse reporting to find some random code. Now, there is one use case, a theorical one again func (a A) Foo() *A {
a.whatever = true
return &a
} Should this one be reported or not? This one could be the code of a Clone method that reset a few fields func (a A) Clone() *A {
a.whatever = true
return &a
} So here again, a code that seems legitimate too. Is there any counter examples you may think about of a code returning a pointer? |
I don't see any real difference between returning a data or a pointer - this depends on what user expect to get, not on method implementation details. E.g. if user gets a pointer from |
IMO, do not report a failure (considered as false positive)
IMO, do not report a failure (considered as false positive) |
Is it covered by the current code (the one in a dedicated branch) ? I mean will this be marked as false positive and the later ignored (no computer with me now, I'm on phone) func (l Logger) Clone() (Logger, error) {
l.whatever = true
return l
} |
Thanks for your time and patience @powerman @chavacava |
Is your feature request related to a problem? Please describe.
In some cases using immutable values can be a good style. For example, I often use
type Config struct{...}
this way, to have both (a variant of) dysfunctional options and ensure there are no references to config (or it values) outside of object using that config:This result in clean and safe code, but revive thinks it's bad:
Describe the solution you'd like
I'd like to make it possible to not trigger
modifies-value-receiver
rule in case method returns value of receiver type (or a pointer to receiver type). Examples:The reason to support references in returned value is: both
NewConfig()
andWithX()
methods may return*Config
without actually changing whole pattern. This is because copy will anyway happens when it will be used as non-reference argument (as a receiver forWithX()
or as an argument forNewThing()
. Using reference may make sense, e.g. ifConfig
is huge, or just to makeNewConfig()
looks more consistent (New
usually means return reference).I think it's safe to make
modifies-value-receiver
work this way by default, but if you unsure it can be configurable.Describe alternatives you've considered
Adding
//nolint:revive
or disablingmodifies-value-receiver
rule. First is annoying, second is unsafe.Additional context
N/A
The text was updated successfully, but these errors were encountered: