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

[pylint] Implement use-implicit-booleaness-not-len (PLC1802) #14309

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Lokejoke
Copy link
Contributor

@Lokejoke Lokejoke commented Nov 13, 2024

Summary

This PR implements use-implicit-booleaness-not-len / C1802

For sequences, (strings, lists, tuples), use the fact that empty sequences are false.

Test Plan

Checked against pylint tests:

  • I would argue some of the tests (commented out) are not very intuitive and thus not helpful

Notes

  • Naming: Rule name and its error message (adopted from pylint) do not follow conventions used in Ruff (len-as-condition might be solution)

References

PEP 8 on empty sequences and implicit booleaness

@Lokejoke Lokejoke marked this pull request as draft November 13, 2024 08:47
Copy link
Contributor

github-actions bot commented Nov 13, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+16 -0 violations, +0 -0 fixes in 5 projects; 49 projects unchanged)

apache/airflow (+7 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ dev/breeze/src/airflow_breeze/commands/release_management_commands.py:1624:8: PLC1802 [*] `len(success_entries)` used as condition without comparison
+ dev/breeze/src/airflow_breeze/commands/release_management_commands.py:1631:8: PLC1802 [*] `len(skipped_entries)` used as condition without comparison
+ dev/breeze/src/airflow_breeze/commands/release_management_commands.py:1736:12: PLC1802 [*] `len(success_entries)` used as condition without comparison
+ dev/breeze/src/airflow_breeze/commands/release_management_commands.py:1743:12: PLC1802 [*] `len(skipped_entries)` used as condition without comparison
+ providers/src/airflow/providers/amazon/aws/hooks/glue.py:264:16: PLC1802 [*] `len(fetched_logs)` used as condition without comparison
+ providers/src/airflow/providers/sftp/sensors/sftp.py:128:16: PLC1802 [*] `len(files_found)` used as condition without comparison
+ providers/src/airflow/providers/snowflake/operators/snowflake.py:559:20: PLC1802 [*] `len(queries_in_progress)` used as condition without comparison

apache/superset (+5 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ superset/databases/filters.py:98:16: PLC1802 [*] `len(allowed_schemas)` used as condition without comparison
+ superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py:444:61: PLC1802 [*] `len(SEQUENCE)` used as condition without comparison
+ superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py:546:11: PLC1802 [*] `len(ordered_raw_positions)` used as condition without comparison
+ superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py:559:16: PLC1802 [*] `len(available_columns_index)` used as condition without comparison
+ superset/migrations/versions/2018-11-12_13-31_4ce8df208545_migrate_time_range_for_default_filters.py:68:24: PLC1802 [*] `len(keys)` used as condition without comparison

bokeh/bokeh (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ src/bokeh/core/validation/check.py:216:27: PLC1802 [*] `len(warnings)` used as condition without comparison

zulip/zulip (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ zerver/views/invite.py:127:8: PLC1802 [*] `len(streams)` used as condition without comparison
+ zerver/views/invite.py:243:8: PLC1802 [*] `len(streams)` used as condition without comparison

pytest-dev/pytest (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview

+ testing/_py/test_local.py:218:16: PLC1802 [*] `len(lst)` used as condition without comparison

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
PLC1802 16 16 0 0 0

@Lokejoke Lokejoke marked this pull request as ready for review November 13, 2024 10:55
@Lokejoke Lokejoke changed the title [pylint] use-implicit-booleaness-not-len (C1802) [pylint] Implement use-implicit-booleaness-not-len (C1802) Nov 14, 2024
@Lokejoke Lokejoke changed the title [pylint] Implement use-implicit-booleaness-not-len (C1802) [pylint] Implement use-implicit-booleaness-not-len (C1802) Nov 14, 2024
@Lokejoke Lokejoke changed the title [pylint] Implement use-implicit-booleaness-not-len (C1802) [pylint] Implement use-implicit-booleaness-not-len (C1802) Nov 14, 2024
@Lokejoke Lokejoke changed the title [pylint] Implement use-implicit-booleaness-not-len (C1802) [pylint] Implement use-implicit-booleaness-not-len (PLC1802) Nov 14, 2024
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.

Thanks so much for this contribution! Some minor nits/requests for a few more tests. I also didn't get a chance to review the implementation carefully, but I'll hold off for now in case the new tests inspire any changes.

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 test some more complicated examples involving rebindings/scope since you use those properties of the semantic model? Maybe something like:

def f(cond:bool):
    x = [1,2,3]
    if cond:
        x = [4,5,6]
    if len(x):
        print(x)

and

def g(cond:bool):
    x = [1,2,3]
    if cond:
        x = [4,5,6]
    if len(x):
        print(x)
    del x

and

def h(cond:bool):
    x = [1,2,3]
    x = 123
    if len(x):
        print(x)

and

def outer():
    x = [1,2,3]
    def inner(x:int):
        return x+1
    if len(x):
        print(x)

or any others you can think of to exercise the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, this implementation does support only one name assignment in current scope.
It would be fair to check all prior assignments to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure, there are 3 options:

  • check only those without rebindings (as is now)
  • check recursively if all of possible rebindings bounds to a sequence-like object
  • leave it for type inference

@dylwil3 dylwil3 added rule Implementing or modifying a lint rule preview Related to preview mode features labels Nov 14, 2024
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.

This looks really great, thank you! I think even with the possible misses around control flow, this makes sense to implement. You would have to be pretty pathological to create a custom class C that implements __len__ and __bool__ where bool(x) != bool(len(x)) for x: C... so I think it's okay.

| PythonType::List
| PythonType::Set
| PythonType::Tuple
| PythonType::Generator
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do generators have a length? I think len((x for x in range(10)) gives a TypeError, whereas (x for x in range(10)) is truthy, so these would not have the same behavior in a boolean test.


// Attempt to find the binding's value
let Some(binding_value) = find_binding_value(binding, semantic) else {
// check for `vararg`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice! Should we also see if its bound to kwargs? Those should have a length too.

// Match against specific built-in constructors that return sequences
return semantic
.resolve_builtin_symbol(func)
.is_some_and(|func| matches!(func, "list" | "set" | "dict" | "tuple"));
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about range, str, repr, or other builtins that return sets, dicts, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm gonna explore that, good point.

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.

3 participants