Skip to content

[refurb] Add coverage for using set(...) in single-item-membership-test (FURB171)#15793

Closed
naslundx wants to merge 1 commit intoastral-sh:mainfrom
naslundx:single-item-membership-test-missing-set-constructor
Closed

[refurb] Add coverage for using set(...) in single-item-membership-test (FURB171)#15793
naslundx wants to merge 1 commit intoastral-sh:mainfrom
naslundx:single-item-membership-test-missing-set-constructor

Conversation

@naslundx
Copy link
Contributor

Summary

Adds coverage of using set(...) in addition to `{...} in SingleItemMembershipTest.

Fixes #15792

Test Plan

Updated unit test and snapshot.
Steps to reproduce are in the issue linked above.

@naslundx naslundx force-pushed the single-item-membership-test-missing-set-constructor branch from 77168c6 to 781af24 Compare January 28, 2025 20:17
@github-actions
Copy link
Contributor

github-actions bot commented Jan 28, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@naslundx
Copy link
Contributor Author

naslundx commented Jan 28, 2025

Ah, I see, it doesn't work if the single argument to set() is an enum, for example. :(

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Thanks! However, I think it would be incorrect for this rule to trigger on set(1), since that doesn't create a set with a single member at runtime. It raises an exception.

Comment on lines 12 to 28
if 1 in set(1):
print("Single-element set")
Copy link
Member

Choose a reason for hiding this comment

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

hmm, this fails at runtime:

>>> set(1)
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    set(1)
    ~~~^^^
TypeError: 'int' object is not iterable

Comment on lines 35 to 66
if 1 in set(1, 2):
pass
Copy link
Member

Choose a reason for hiding this comment

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

this also fails at runtime:

>>> set(1, 2)
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    set(1, 2)
    ~~~^^^^^^
TypeError: set expected at most 1 argument, got 2

@AlexWaygood
Copy link
Member

AlexWaygood commented Jan 29, 2025

To clarify, @naslundx, all of these succeed at runtime, and I'd be okay with adding support for these to FURB171!

set([1])
set((1,))
set({1})

You'd need to check that the object being passed to the set() call is an Expr::List, Expr::Tuple or Expr::Set that has exactly one member in its elts field.

@InSyncWithFoo
Copy link
Contributor

InSyncWithFoo commented Jan 29, 2025

You'd need to check that the object [...] has exactly one member in its elts field.

Additionally, that element should be literal/hashable, so that errors like this one are not masked:

if a in set([{}]): ...  # TypeError: unhashable type: 'dict'
if a == {}: ...         # No errors

@dylwil3
Copy link
Collaborator

dylwil3 commented Jan 30, 2025

You'd need to check that the object [...] has exactly one member in its elts field.

Additionally, that element should be literal/hashable, so that errors like this one are not masked:

if a in set([{}]): ...  # TypeError: unhashable type: 'dict'
if a == {}: ...         # No errors

I think we should open a separate issue about this because that behavior is already present in the current scope of the rule:

1 in {set()} # TypeError: unhashable type: 'set'
1 == set() # No errors

@dylwil3 dylwil3 added rule Implementing or modifying a lint rule preview Related to preview mode features labels Jan 30, 2025
@dylwil3 dylwil3 changed the title (FURB171) Add coverage of using set(...) in SingleItemMembershipTest [refurb] Add coverage for using set(...) in single-item-membership-test (FURB171) Jan 30, 2025
@tdulcet
Copy link

tdulcet commented Jan 30, 2025

Maybe check for frozenset as well:

frozenset([1])
frozenset((1,))
frozenset({1})

@naslundx
Copy link
Contributor Author

naslundx commented Mar 3, 2025

I did not see the updated comments on this PR - I will bring it back up to speed. Thanks for comments!

@naslundx naslundx force-pushed the single-item-membership-test-missing-set-constructor branch from 781af24 to ffc2c83 Compare March 4, 2025 08:47
@MichaReiser MichaReiser requested a review from AlexWaygood March 4, 2025 09:01
@AlexWaygood AlexWaygood requested a review from dylwil3 March 4, 2025 10:30
@dylwil3 dylwil3 self-assigned this Mar 4, 2025
Copy link
Collaborator

@dylwil3 dylwil3 left a comment

Choose a reason for hiding this comment

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

Looking good, thank you! Let's add a little more test coverage, and handle the case where someone is shadowing or explicitly importing the builtin.

Comment on lines +132 to +141
if let Expr::Name(ast::ExprName {
id,
ctx: _,
range: _,
}) = func.as_ref()
{
if (id == "set" || id == "frozenset") && arguments.args.len() == 1 {
return single_item(&arguments.args[0]);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should use this method to make sure the function matches the builtin that you expect:

/// Return `true` if `expr` is a reference to `builtins.$target`,
/// i.e. either `object` (where `object` is not overridden in the global scope),
/// or `builtins.object` (where `builtins` is imported as a module at the top level)
pub fn match_builtin_expr(&self, expr: &Expr, symbol: &str) -> bool {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did not know about this, that's great!

Comment on lines +18 to +20
if 1 in set({1}):
print("Single-element set")

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add some examples with nested calls to set/frozenset since your implementation is recursive? Like

set(set([1]))

Also a few non-examples, like:

set(1,) # this is a TypeError so we shouldn't do anything with it
set(1,2) # this is also a TypeError
set((x for x in range(2))) # this is legal but not one element

@AlexWaygood AlexWaygood removed their request for review March 17, 2025 16:45
@MichaReiser
Copy link
Member

Thanks @naslundx for working on this. I'll close this PR because it has become stale. For anyone interested, feel free to open a new PR implementing this change, incorporating the feedback from this review.

@naslundx
Copy link
Contributor Author

naslundx commented May 4, 2025

A little late to the party but I made the fixes, and will put up an updated PR

ntBre pushed a commit that referenced this pull request May 29, 2025
…18035)

## Summary

Adds coverage of using set(...) in addition to `{...} in
SingleItemMembershipTest.

Fixes #15792
(and replaces the old PR #15793)

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

Updated unit test and snapshot.

Steps to reproduce are in the issue linked above.

<!-- How was it tested? -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FURB171 Does not trigger when calling set(...)

6 participants