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

Implement PEP 604 annotation rewrites #369

Merged
merged 1 commit into from
Oct 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
| U005 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 |
| U007 | UsePEP604Annotation | Use `X | Y` for type annotations | | 🛠 |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |

## Integrations
Expand Down
40 changes: 40 additions & 0 deletions resources/test/fixtures/U007.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Optional


def f(x: Optional[str]) -> None:
...


import typing


def f(x: typing.Optional[str]) -> None:
...


from typing import Union


def f(x: Union[str, int, Union[float, bytes]]) -> None:
...


import typing


def f(x: typing.Union[str, int]) -> None:
...


from typing import Union


def f(x: "Union[str, int, Union[float, bytes]]") -> None:
...


import typing


def f(x: "typing.Union[str, int]") -> None:
...
1 change: 1 addition & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod checks;
pub mod helpers;
pub mod operations;
pub mod relocate;
pub mod types;
Expand Down
9 changes: 9 additions & 0 deletions src/ast/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use rustpython_ast::{Expr, ExprKind};

pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
ExprKind::Attribute { attr, .. } => target == attr,
ExprKind::Name { id, .. } => target == id,
_ => false,
}
}
48 changes: 36 additions & 12 deletions src/check_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ use std::path::Path;
use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::Location;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
};
use rustpython_parser::parser;

use crate::ast::helpers::match_name_or_attr;
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Expand Down Expand Up @@ -100,14 +102,6 @@ impl<'a> Checker<'a> {
}
}

fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
ExprKind::Attribute { attr, .. } => target == attr,
ExprKind::Name { id, .. } => target == id,
_ => false,
}
}

enum SubscriptKind {
AnnotatedSubscript,
PEP593AnnotatedSubscript,
Expand Down Expand Up @@ -709,7 +703,14 @@ where

// Pre-visit.
match &expr.node {
ExprKind::Subscript { value, .. } => {
ExprKind::Subscript { value, slice, .. } => {
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U007)
&& self.settings.target_version >= PythonVersion::Py39
{
plugins::use_pep604_annotation(self, expr, value, slice);
}

if match_name_or_attr(value, "Literal") {
self.in_literal = true;
}
Expand Down Expand Up @@ -1605,14 +1606,37 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
while let Some((location, expression)) = self.deferred_string_annotations.pop() {
while let Some((range, expression)) = self.deferred_string_annotations.pop() {
// HACK(charlie): We need to modify `range` such that it represents the range of the
// expression _within_ the string annotation (as opposed to the range of the string
// annotation itself). RustPython seems to return an off-by-one start column for every
// string value, so we check for double quotes (which are really triple quotes).
let contents = self.locator.slice_source_code_at(&range.location);
let range = if contents.starts_with("\"\"") || contents.starts_with("\'\'") {
Range {
location: Location::new(range.location.row(), range.location.column() + 2),
end_location: Location::new(
range.end_location.row(),
range.end_location.column() - 2,
),
}
} else {
Range {
location: Location::new(range.location.row(), range.location.column()),
end_location: Location::new(
range.end_location.row(),
range.end_location.column() - 1,
),
}
};

if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, location);
relocate_expr(&mut expr, range);
allocator.push(expr);
} else if self.settings.enabled.contains(&CheckCode::F722) {
self.checks.push(Check::new(
CheckKind::ForwardAnnotationSyntaxError(expression.to_string()),
self.locate_check(location),
self.locate_check(range),
));
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub enum CheckCode {
U004,
U005,
U006,
U007,
// Meta
M001,
}
Expand Down Expand Up @@ -230,6 +231,7 @@ pub enum CheckKind {
NoAssertEquals,
UselessObjectInheritance(String),
UsePEP585Annotation(String),
UsePEP604Annotation,
// Meta
UnusedNOQA(Option<String>),
}
Expand Down Expand Up @@ -322,6 +324,7 @@ impl CheckCode {
CheckCode::U004 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::U005 => CheckKind::NoAssertEquals,
CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()),
CheckCode::U007 => CheckKind::UsePEP604Annotation,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
Expand Down Expand Up @@ -401,6 +404,7 @@ impl CheckKind {
CheckKind::UselessMetaclassType => &CheckCode::U001,
CheckKind::NoAssertEquals => &CheckCode::U005,
CheckKind::UsePEP585Annotation(_) => &CheckCode::U006,
CheckKind::UsePEP604Annotation => &CheckCode::U007,
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
Expand Down Expand Up @@ -603,6 +607,7 @@ impl CheckKind {
name,
)
}
CheckKind::UsePEP604Annotation => "Use `X | Y` for type annotations".to_string(),
// Meta
CheckKind::UnusedNOQA(code) => match code {
None => "Unused `noqa` directive".to_string(),
Expand All @@ -626,6 +631,7 @@ impl CheckKind {
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(_)
| CheckKind::UsePEP585Annotation(_)
| CheckKind::UsePEP604Annotation
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/code_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ impl SourceGenerator {
Ok(())
}

fn unparse_expr<U>(&mut self, ast: &Expr<U>, level: u8) -> fmt::Result {
pub fn unparse_expr<U>(&mut self, ast: &Expr<U>, level: u8) -> fmt::Result {
macro_rules! opprec {
($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => {
match $x {
Expand Down
12 changes: 12 additions & 0 deletions src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1001,4 +1001,16 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}

#[test]
fn u007() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U007.py"),
&settings::Settings::for_rule(CheckCode::U007),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod super_call_with_parameters;
mod type_of_primitive;
mod unnecessary_abspath;
mod use_pep585_annotation;
mod use_pep604_annotation;
mod useless_metaclass_type;
mod useless_object_inheritance;

Expand All @@ -19,5 +20,6 @@ pub use super_call_with_parameters::super_call_with_parameters;
pub use type_of_primitive::type_of_primitive;
pub use unnecessary_abspath::unnecessary_abspath;
pub use use_pep585_annotation::use_pep585_annotation;
pub use use_pep604_annotation::use_pep604_annotation;
pub use useless_metaclass_type::useless_metaclass_type;
pub use useless_object_inheritance::useless_object_inheritance;
77 changes: 77 additions & 0 deletions src/plugins/use_pep604_annotation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use anyhow::{anyhow, Result};
use rustpython_ast::{Expr, ExprKind};

use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::code_gen::SourceGenerator;

pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
if match_name_or_attr(value, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(slice, 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content: format!("{} | None", content),
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
} else if match_name_or_attr(value, "Union") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
match &slice.node {
ExprKind::Slice { .. } => {
// Invalid type annotation.
}
ExprKind::Tuple { elts, .. } => {
// Multiple arguments.
let parts: Result<Vec<String>> = elts
.iter()
.map(|expr| {
let mut generator = SourceGenerator::new();
generator
.unparse_expr(expr, 0)
.map_err(|_| anyhow!("Failed to parse."))?;
generator
.generate()
.map_err(|_| anyhow!("Failed to generate."))
})
.collect();
if let Ok(parts) = parts {
let content = parts.join(" | ");
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
_ => {
// Single argument.
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(slice, 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
}
}
}
}
checker.add_check(check);
}
}
2 changes: 1 addition & 1 deletion src/snapshots/ruff__linter__tests__f722.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ expression: checks
column: 13
end_location:
row: 9
column: 17
column: 16
fix: ~

8 changes: 4 additions & 4 deletions src/snapshots/ruff__linter__tests__f821.snap
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ expression: checks
column: 5
end_location:
row: 58
column: 9
column: 8
fix: ~
- kind:
UndefinedName: TOMATO
Expand Down Expand Up @@ -81,7 +81,7 @@ expression: checks
column: 10
end_location:
row: 114
column: 24
column: 23
fix: ~
- kind:
UndefinedName: foo
Expand All @@ -90,7 +90,7 @@ expression: checks
column: 15
end_location:
row: 122
column: 19
column: 18
fix: ~
- kind:
UndefinedName: bar
Expand All @@ -99,6 +99,6 @@ expression: checks
column: 22
end_location:
row: 122
column: 26
column: 25
fix: ~

Loading