Add autofix for Set-to-AbstractSet rewrite using reference tracking (#5074)

## Summary

This PR enables autofix behavior for the `flake8-pyi` rule that asks you
to alias `Set` to `AbstractSet` when importing `collections.abc.Set`.
It's not the most important rule, but it's a good isolated test-case for
local symbol renaming.

The renaming algorithm is outlined in-detail in the `renamer.rs` module.
But to demonstrate the behavior, here's the diff when running this fix
over a complex file that exercises a few edge cases:

```diff
--- a/foo.pyi
+++ b/foo.pyi
@@ -1,16 +1,16 @@
 if True:
-    from collections.abc import Set
+    from collections.abc import Set as AbstractSet
 else:
-    Set = 1
+    AbstractSet = 1

-x: Set = set()
+x: AbstractSet = set()

-x: Set
+x: AbstractSet

-del Set
+del AbstractSet

 def f():
-    print(Set)
+    print(AbstractSet)

     def Set():
         pass
```

Making this work required resolving a bunch of edge cases in the
semantic model that were causing us to "lose track" of references. For
example, the above wasn't possible with our previous approach to
handling deletions (#5071). Similarly, the `x: Set` "delayed annotation"
tracking was enabled via #5070. And many of these edits would've failed
if we hadn't changed `BindingKind` to always match the identifier range
(#5090). So it's really the culmination of a bunch of changes over the
course of the week.

The main outstanding TODO is that this doesn't support `global` or
`nonlocal` usages. I'm going to take a look at that tonight, but I'm
comfortable merging this as-is.

Closes #1106.

Closes #5091.
This commit is contained in:
Charlie Marsh 2023-06-16 10:12:33 -04:00 committed by GitHub
parent 307f7a735c
commit b9754bd5c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 414 additions and 100 deletions

View file

@ -44,6 +44,18 @@ impl<'a> Binding<'a> {
self.flags.contains(BindingFlags::EXPLICIT_EXPORT)
}
/// Return `true` if this [`Binding`] represents an external symbol
/// (e.g., `FastAPI` in `from fastapi import FastAPI`).
pub const fn is_external(&self) -> bool {
self.flags.contains(BindingFlags::EXTERNAL)
}
/// Return `true` if this [`Binding`] represents an aliased symbol
/// (e.g., `app` in `from fastapi import FastAPI as app`).
pub const fn is_alias(&self) -> bool {
self.flags.contains(BindingFlags::ALIAS)
}
/// Return `true` if this [`Binding`] represents an unbound variable
/// (e.g., `x` in `x = 1; del x`).
pub const fn is_unbound(&self) -> bool {
@ -161,9 +173,25 @@ bitflags! {
///
/// For example, the binding could be `FastAPI` in:
/// ```python
/// import FastAPI as FastAPI
/// from fastapi import FastAPI as FastAPI
/// ```
const EXPLICIT_EXPORT = 1 << 0;
/// The binding represents an external symbol, like an import or a builtin.
///
/// For example, the binding could be `FastAPI` in:
/// ```python
/// from fastapi import FastAPI
/// ```
const EXTERNAL = 1 << 1;
/// The binding is an aliased symbol.
///
/// For example, the binding could be `app` in:
/// ```python
/// from fastapi import FastAPI as app
/// ```
const ALIAS = 1 << 2;
}
}