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

U1000: reduce false positive that relates to type parameters #1448

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

seiyab
Copy link

@seiyab seiyab commented Sep 24, 2023

Addresses #1294.

NOTE
Probably some false-positives for complex generic types still exist. And this doesn't address other similar problems such as #1440.

Though I consider this makes a improvement, feel free to reject this if you feel this incomplete fix can be harmful.

@seiyab
Copy link
Author

seiyab commented Nov 1, 2023

@dominikh any chance to get reviewed?

@dominikh
Copy link
Owner

dominikh commented Nov 1, 2023

I'll take a look when I get the chance

@dominikh
Copy link
Owner

dominikh commented Nov 6, 2023

I'm wondering if instead we can use https://pkg.go.dev/golang.org/x/exp/typeparams#GenericAssignableTo — that is, replace our implementation of implements with a modified version of GenericAssignableTo.

@seiyab
Copy link
Author

seiyab commented Nov 6, 2023

Thank you for your review. I'll take a look.

@seiyab
Copy link
Author

seiyab commented Nov 7, 2023

I locally tried https://pkg.go.dev/golang.org/x/exp/typeparams#GenericAssignableTo and it successfully passed the tests.
Perhaps it might also resolve opposite false positive as #1440 reports. I'll investigate more and might add more tests.

@seiyab
Copy link
Author

seiyab commented Nov 7, 2023

Ah, I mistakenly tested ./statickcheck/... instead of ./unused/.... Testing ./unused/..., it fails. I'll investigate it anyway.

@seiyab
Copy link
Author

seiyab commented Nov 7, 2023

@dominikh
I found that we can't utilize types.GenericAssignableTo.
types.GenericAssignableTo looks to consider V and T have same numbers of type parameters. (If not, it falls back types.AssignableTo)

If V and T are generic named types, then V is considered assignable to T if, for every possible instantation of V[A_1, ..., A_N], the instantiation T[A_1, ..., A_N] is valid and V[A_1, ..., A_N] implements T[A_1, ..., A_N].

In contrast, to resolve #1294, we want to check whether concrete type V is assignable to one of instantiation of T[A_1, ..., A_N] or not.

Copy link
Owner

@dominikh dominikh left a comment

Choose a reason for hiding this comment

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

Overall the code looks fine.

I'm very torn on the overall change, however. On the one hand, this fixes some of the common false positives. On the other hand, the incomplete nature of the change means that minor changes to an interface can reintroduce a false positive.

For example, we'd be able to correctly handle type I[T any] interface { foo() T }, but not type I[T any] interface { foo() []T }. This might be more confusing for users, who will not understand why a minor change to the interface introduces a new false positive.

/cc @mvdan for his opinion.

Note that I don't think we can realistically solve this problem unless we reuse the whole unification step of go/types, which unfortunately isn't currently exported. I'll look into rectifying that.


func satisfiesConstraint(t types.Type, tp *types.TypeParam) bool {
bound, ok := tp.Underlying().(*types.Interface)
if !ok {
Copy link
Owner

Choose a reason for hiding this comment

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

types.TypeParam.Underlying is guaranteed to be a *types.Interface (for valid programs, and only valid programs reach our checks), so we can drop the , ok — I'd rather this panic if that invariant is ever violated.

if !ok {
return false
}
return types.Implements(t, bound)
Copy link
Owner

Choose a reason for hiding this comment

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

This should probably use types.Satisfies

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, changing it into types.Satisfies() make some tests fail and clarify that I'm misunderstanding something around here. Probably bound should also be tp.Constraint() or something but it sometimes returns Type which isn't *types.Interface so it can't obviously passed to types.Satisfies().
I'll investigate it and add more test cases.

}

func newMethodChecker() *methodsChecker {
return &methodsChecker{
Copy link
Owner

Choose a reason for hiding this comment

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

I'd like to avoid this allocation when there are no type parameters.

if types.Identical(implType, interfaceType) {
return true
}
tp, ok := interfaceType.(*types.TypeParam)
Copy link
Owner

Choose a reason for hiding this comment

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

This won't support methods like foo(x []T). We should at least have a comment here to document this limitation.

Copy link
Owner

Choose a reason for hiding this comment

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

This still needs a comment stating that we only support trivial uses of type parameters and that this doesn't constitute full unification.

@dominikh
Copy link
Owner

dominikh commented Nov 7, 2023

@dominikh I found that we can't utilize types.GenericAssignableTo. types.GenericAssignableTo looks to consider V and T have same numbers of type parameters. (If not, it falls back types.AssignableTo)

If V and T are generic named types, then V is considered assignable to T if, for every possible instantation of V[A_1, ..., A_N], the instantiation T[A_1, ..., A_N] is valid and V[A_1, ..., A_N] implements T[A_1, ..., A_N].

In contrast, to resolve #1294, we want to check whether concrete type V is assignable to one of instantiation of T[A_1, ..., A_N] or not.

Thank you for looking into this.

@dominikh
Copy link
Owner

dominikh commented Nov 7, 2023

Thinking more about this, I think we should merge this PR, then improve upon it with better/full unification in a future change.

@seiyab
Copy link
Author

seiyab commented Nov 9, 2023

@dominikh Please take another look.
Thank you for your review. Some my confusions are solved and it get better.

For avoiding allocation, I can't find a lightweight approach to know whether a *type.Interface has any type parameters or not. So I use a value of methodChecker instead its reference. It can't go out of the function implements() scope and should be put on stack. And I also make() typeParams map lazily.

@mvdan
Copy link
Sponsor Contributor

mvdan commented Nov 17, 2023

@dominikh I think your analysis above is sensible. I think we should push go/types to expose more APIs if they are useful for tools.

@seiyab seiyab requested a review from dominikh November 17, 2023 14:48
@dominikh
Copy link
Owner

@dominikh I think your analysis above is sensible. I think we should push go/types to expose more APIs if they are useful for tools.

FWIW, I've filed golang/go#63982

typeParams map[*types.TypeParam]types.Type
}

func newMethodChecker() methodsChecker {
Copy link
Owner

Choose a reason for hiding this comment

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

We don't need this constructor.

@@ -60,6 +62,7 @@ func implements(V types.Type, T *types.Interface, msV *types.MethodSet) ([]*type

// A concrete type implements T if it implements all methods of T.
var sels []*types.Selection
c := newMethodChecker()
Copy link
Owner

Choose a reason for hiding this comment

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

This can simply be var c methodsChecker

func satisfiesConstraint(t types.Type, tp *types.TypeParam) bool {
bound, ok := tp.Constraint().Underlying().(*types.Interface)
if !ok {
panic("unexpected failure on type assertion (types.Type to *types.Interface)")
Copy link
Owner

Choose a reason for hiding this comment

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

Just do bound := tp.Constraint().Underlying().(*types.Interface) — we don't need to check for ok just to manually panic — the panic we get on the failed type assertion is fine.

if types.Identical(implType, interfaceType) {
return true
}
tp, ok := interfaceType.(*types.TypeParam)
Copy link
Owner

Choose a reason for hiding this comment

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

This still needs a comment stating that we only support trivial uses of type parameters and that this doesn't constitute full unification.

- remove constructor
- remove manual panic
- add a comment
@seiyab seiyab requested a review from dominikh November 18, 2023 04:09
@seiyab
Copy link
Author

seiyab commented Nov 18, 2023

@dominikh Thank you for your review. I fixed them.

Copy link
Owner

@dominikh dominikh left a comment

Choose a reason for hiding this comment

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

LGTM, will merge in a couple days.

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.

None yet

3 participants