Implement experimental binding support (#28)

This commit is contained in:
Charlie Marsh 2022-08-27 18:16:19 -04:00 committed by GitHub
parent 6129d5bbba
commit 618915f09e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 473 additions and 255 deletions

6
Cargo.lock generated
View File

@ -1727,7 +1727,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-ast" name = "rustpython-ast"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=e9970edd775a617226007d3d3cb0fbbc39ed3948#e9970edd775a617226007d3d3cb0fbbc39ed3948" source = "git+https://github.com/RustPython/RustPython.git?rev=7a688e0b6c2904f286acac1e4af3f1628dd38589#7a688e0b6c2904f286acac1e4af3f1628dd38589"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"rustpython-compiler-core", "rustpython-compiler-core",
@ -1736,7 +1736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-compiler-core" name = "rustpython-compiler-core"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=e9970edd775a617226007d3d3cb0fbbc39ed3948#e9970edd775a617226007d3d3cb0fbbc39ed3948" source = "git+https://github.com/RustPython/RustPython.git?rev=7a688e0b6c2904f286acac1e4af3f1628dd38589#7a688e0b6c2904f286acac1e4af3f1628dd38589"
dependencies = [ dependencies = [
"bincode", "bincode",
"bitflags", "bitflags",
@ -1753,7 +1753,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-parser" name = "rustpython-parser"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=e9970edd775a617226007d3d3cb0fbbc39ed3948#e9970edd775a617226007d3d3cb0fbbc39ed3948" source = "git+https://github.com/RustPython/RustPython.git?rev=7a688e0b6c2904f286acac1e4af3f1628dd38589#7a688e0b6c2904f286acac1e4af3f1628dd38589"
dependencies = [ dependencies = [
"ahash", "ahash",
"anyhow", "anyhow",

View File

@ -23,7 +23,7 @@ notify = { version = "4.0.17" }
pyo3 = { version = "0.16.5", features = ["extension-module", "abi3-py37"] } pyo3 = { version = "0.16.5", features = ["extension-module", "abi3-py37"] }
rayon = { version = "1.5.3" } rayon = { version = "1.5.3" }
regex = { version = "1.6.0" } regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "e9970edd775a617226007d3d3cb0fbbc39ed3948" } rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "7a688e0b6c2904f286acac1e4af3f1628dd38589" }
serde = { version = "1.0.143", features = ["derive"] } serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" } serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" } toml = { version = "0.5.9" }

View File

@ -0,0 +1,13 @@
import os
import functools
from collections import (
Counter,
OrderedDict,
)
class X:
def a(self):
x = os.environ["1"]
y = Counter()
return X

View File

@ -0,0 +1,6 @@
from F634 import *
from F634 import * # noqa: E501
from F634 import * # noqa
from F634 import * # noqa: F403
from F634 import * # noqa: F403, E501

View File

@ -7,3 +7,5 @@ e = (
f"def" + f"def" +
"ghi" "ghi"
) )
g = f"ghi{123:{45}}"

View File

@ -1,6 +0,0 @@
from if_tuple import *
from if_tuple import * # noqa: E501
from if_tuple import * # noqa
from if_tuple import * # noqa: F403
from if_tuple import * # noqa: F403, E501

View File

@ -1,8 +1,10 @@
use std::collections::BTreeSet; use std::collections::{BTreeMap, BTreeSet};
use rustpython_parser::ast::{
Arg, Arguments, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Suite,
};
use crate::check_ast::ScopeKind::{Class, Function, Generator, Module}; use crate::check_ast::ScopeKind::{Class, Function, Generator, Module};
use rustpython_parser::ast::{Arg, Arguments, Expr, ExprKind, Stmt, StmtKind, Suite};
use crate::checks::{Check, CheckKind}; use crate::checks::{Check, CheckKind};
use crate::settings::Settings; use crate::settings::Settings;
use crate::visitor; use crate::visitor;
@ -17,12 +19,33 @@ enum ScopeKind {
struct Scope { struct Scope {
kind: ScopeKind, kind: ScopeKind,
values: BTreeMap<String, Binding>,
}
enum BindingKind {
Argument,
Assignment,
ClassDefinition,
Definition,
FutureImportation,
Importation,
StarImportation,
SubmoduleImportation,
}
struct Binding {
kind: BindingKind,
name: String,
location: Location,
used: bool,
} }
struct Checker<'a> { struct Checker<'a> {
settings: &'a Settings, settings: &'a Settings,
checks: Vec<Check>, checks: Vec<Check>,
scopes: Vec<Scope>, scopes: Vec<Scope>,
dead_scopes: Vec<Scope>,
in_f_string: bool,
} }
impl Checker<'_> { impl Checker<'_> {
@ -30,7 +53,9 @@ impl Checker<'_> {
Checker { Checker {
settings, settings,
checks: vec![], checks: vec![],
scopes: vec![Scope { kind: Module }], scopes: vec![],
dead_scopes: vec![],
in_f_string: false,
} }
} }
} }
@ -38,8 +63,30 @@ impl Checker<'_> {
impl Visitor for Checker<'_> { impl Visitor for Checker<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) { fn visit_stmt(&mut self, stmt: &Stmt) {
match &stmt.node { match &stmt.node {
StmtKind::FunctionDef { .. } => self.scopes.push(Scope { kind: Function }), StmtKind::FunctionDef { name, .. } => {
StmtKind::AsyncFunctionDef { .. } => self.scopes.push(Scope { kind: Function }), self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
});
self.add_binding(Binding {
kind: BindingKind::ClassDefinition,
name: name.clone(),
used: false,
location: stmt.location,
})
}
StmtKind::AsyncFunctionDef { name, .. } => {
self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
});
self.add_binding(Binding {
kind: BindingKind::ClassDefinition,
name: name.clone(),
used: false,
location: stmt.location,
})
}
StmtKind::Return { .. } => { StmtKind::Return { .. } => {
if self if self
.settings .settings
@ -59,20 +106,76 @@ impl Visitor for Checker<'_> {
} }
} }
} }
StmtKind::ClassDef { .. } => self.scopes.push(Scope { kind: Class }), StmtKind::ClassDef { .. } => self.push_scope(Scope {
StmtKind::ImportFrom { names, .. } => { kind: Class,
values: BTreeMap::new(),
}),
StmtKind::Import { names } => {
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
self.add_binding(Binding {
kind: BindingKind::SubmoduleImportation,
name: alias.node.name.clone(),
used: false,
location: stmt.location,
})
} else {
self.add_binding(Binding {
kind: BindingKind::Importation,
name: alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone()),
used: false,
location: stmt.location,
})
}
}
}
StmtKind::ImportFrom { names, module, .. } => {
for alias in names {
let name = alias
.node
.asname
.clone()
.unwrap_or_else(|| alias.node.name.clone());
if module
.clone()
.map(|name| name == "future")
.unwrap_or_default()
{
self.add_binding(Binding {
kind: BindingKind::FutureImportation,
name,
used: true,
location: stmt.location,
});
} else if alias.node.name == "*" {
self.add_binding(Binding {
kind: BindingKind::StarImportation,
name,
used: false,
location: stmt.location,
});
if self if self
.settings .settings
.select .select
.contains(CheckKind::ImportStarUsage.code()) .contains(CheckKind::ImportStarUsage.code())
{ {
for alias in names {
if alias.node.name == "*" {
self.checks.push(Check { self.checks.push(Check {
kind: CheckKind::ImportStarUsage, kind: CheckKind::ImportStarUsage,
location: stmt.location, location: stmt.location,
}); });
} }
} else {
self.add_binding(Binding {
kind: BindingKind::Importation,
name,
used: false,
location: stmt.location,
});
} }
} }
} }
@ -117,6 +220,9 @@ impl Visitor for Checker<'_> {
} }
} }
} }
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
}
_ => {} _ => {}
} }
@ -126,18 +232,40 @@ impl Visitor for Checker<'_> {
StmtKind::ClassDef { .. } StmtKind::ClassDef { .. }
| StmtKind::FunctionDef { .. } | StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. } => { | StmtKind::AsyncFunctionDef { .. } => {
self.scopes.pop(); self.pop_scope();
} }
_ => {} _ => {}
}; };
if let StmtKind::ClassDef { name, .. } = &stmt.node {
self.add_binding(Binding {
kind: BindingKind::Definition,
name: name.clone(),
used: false,
location: stmt.location,
});
}
} }
fn visit_expr(&mut self, expr: &Expr) { fn visit_expr(&mut self, expr: &Expr) {
let initial = self.in_f_string;
match &expr.node { match &expr.node {
ExprKind::GeneratorExp { .. } => self.scopes.push(Scope { kind: Generator }), ExprKind::Name { ctx, .. } => match ctx {
ExprKind::Lambda { .. } => self.scopes.push(Scope { kind: Function }), ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => self.handle_node_store(expr),
ExprContext::Del => {}
},
ExprKind::GeneratorExp { .. } => self.push_scope(Scope {
kind: Generator,
values: BTreeMap::new(),
}),
ExprKind::Lambda { .. } => self.push_scope(Scope {
kind: Function,
values: BTreeMap::new(),
}),
ExprKind::JoinedStr { values } => { ExprKind::JoinedStr { values } => {
if self if !self.in_f_string
&& self
.settings .settings
.select .select
.contains(CheckKind::FStringMissingPlaceholders.code()) .contains(CheckKind::FStringMissingPlaceholders.code())
@ -150,6 +278,7 @@ impl Visitor for Checker<'_> {
location: expr.location, location: expr.location,
}); });
} }
self.in_f_string = true;
} }
_ => {} _ => {}
}; };
@ -158,7 +287,12 @@ impl Visitor for Checker<'_> {
match &expr.node { match &expr.node {
ExprKind::GeneratorExp { .. } | ExprKind::Lambda { .. } => { ExprKind::GeneratorExp { .. } | ExprKind::Lambda { .. } => {
self.scopes.pop(); if let Some(scope) = self.scopes.pop() {
self.dead_scopes.push(scope);
}
}
ExprKind::JoinedStr { .. } => {
self.in_f_string = initial;
} }
_ => {} _ => {}
}; };
@ -201,63 +335,99 @@ impl Visitor for Checker<'_> {
visitor::walk_arguments(self, arguments); visitor::walk_arguments(self, arguments);
} }
fn visit_arg(&mut self, arg: &Arg) {
self.add_binding(Binding {
kind: BindingKind::Argument,
name: arg.node.arg.clone(),
used: false,
location: arg.location,
});
visitor::walk_arg(self, arg);
}
}
impl Checker<'_> {
fn push_scope(&mut self, scope: Scope) {
self.scopes.push(scope);
}
fn pop_scope(&mut self) {
self.dead_scopes
.push(self.scopes.pop().expect("Attempted to pop without scope."));
}
fn add_binding(&mut self, binding: Binding) {
// TODO(charlie): Don't treat annotations as assignments if there is an existing value.
let scope = self.scopes.last_mut().expect("No current scope found.");
scope.values.insert(
binding.name.clone(),
match scope.values.get(&binding.name) {
None => binding,
Some(existing) => Binding {
kind: binding.kind,
name: binding.name,
location: binding.location,
used: existing.used,
},
},
);
}
fn handle_node_load(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
for scope in self.scopes.iter_mut().rev() {
if matches!(scope.kind, Class) {
if id == "__class__" {
return;
} else {
continue;
}
}
if let Some(binding) = scope.values.get_mut(id) {
binding.used = true;
}
}
}
}
fn handle_node_store(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
// TODO(charlie): Handle alternate binding types (like `Annotation`).
self.add_binding(Binding {
kind: BindingKind::Assignment,
name: id.to_string(),
used: false,
location: expr.location,
});
}
}
fn check_dead_scopes(&mut self) {
// TODO(charlie): Handle `__all__`.
for scope in &self.dead_scopes {
for (name, binding) in scope.values.iter().rev() {
if !binding.used && matches!(binding.kind, BindingKind::Importation) {
self.checks.push(Check {
kind: CheckKind::UnusedImport(name.clone()),
location: binding.location,
});
}
}
}
}
} }
pub fn check_ast(python_ast: &Suite, settings: &Settings) -> Vec<Check> { pub fn check_ast(python_ast: &Suite, settings: &Settings) -> Vec<Check> {
python_ast
.iter()
.flat_map(|stmt| {
let mut checker = Checker::new(settings); let mut checker = Checker::new(settings);
checker.visit_stmt(stmt); checker.push_scope(Scope {
checker.checks kind: Module,
}) values: BTreeMap::new(),
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use rustpython_parser::ast::{Alias, AliasData, Location, Stmt, StmtKind};
use crate::check_ast::Checker;
use crate::checks::CheckKind::ImportStarUsage;
use crate::checks::{Check, CheckCode};
use crate::settings::Settings;
use crate::visitor::Visitor;
#[test]
fn import_star_usage() {
let settings = Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
};
let mut checker = Checker::new(&settings);
checker.visit_stmt(&Stmt {
location: Location::new(1, 1),
custom: (),
node: StmtKind::ImportFrom {
module: Some("bar".to_string()),
names: vec![Alias::new(
Default::default(),
AliasData {
name: "*".to_string(),
asname: None,
},
)],
level: None,
},
}); });
for stmt in python_ast {
let actual = checker.checks; checker.visit_stmt(stmt);
let expected = vec![Check {
kind: ImportStarUsage,
location: Location::new(1, 1),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
} }
checker.pop_scope();
checker.check_dead_scopes();
checker.checks
} }

View File

@ -3,25 +3,41 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CheckCode { pub enum CheckCode {
F831, E501,
F401,
F403,
F541, F541,
F634, F634,
F403,
F706, F706,
F831,
F901, F901,
E501,
} }
impl CheckCode { impl CheckCode {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
CheckCode::F831 => "F831", CheckCode::E501 => "E501",
CheckCode::F401 => "F401",
CheckCode::F403 => "F403",
CheckCode::F541 => "F541", CheckCode::F541 => "F541",
CheckCode::F634 => "F634", CheckCode::F634 => "F634",
CheckCode::F403 => "F403",
CheckCode::F706 => "F706", CheckCode::F706 => "F706",
CheckCode::F831 => "F831",
CheckCode::F901 => "F901", CheckCode::F901 => "F901",
CheckCode::E501 => "E501", }
}
/// The source for the check (either the AST, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 => &LintSource::Lines,
CheckCode::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST,
CheckCode::F541 => &LintSource::AST,
CheckCode::F634 => &LintSource::AST,
CheckCode::F706 => &LintSource::AST,
CheckCode::F831 => &LintSource::AST,
CheckCode::F901 => &LintSource::AST,
} }
} }
} }
@ -38,25 +54,13 @@ pub enum CheckKind {
FStringMissingPlaceholders, FStringMissingPlaceholders,
IfTuple, IfTuple,
ImportStarUsage, ImportStarUsage,
ReturnOutsideFunction,
RaiseNotImplemented,
LineTooLong, LineTooLong,
RaiseNotImplemented,
ReturnOutsideFunction,
UnusedImport(String),
} }
impl CheckKind { impl CheckKind {
/// Get the CheckKind for a corresponding code.
pub fn new(code: &CheckCode) -> Self {
match code {
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F403 => CheckKind::ImportStarUsage,
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
CheckCode::F901 => CheckKind::RaiseNotImplemented,
CheckCode::E501 => CheckKind::LineTooLong,
}
}
/// A four-letter shorthand code for the check. /// A four-letter shorthand code for the check.
pub fn code(&self) -> &'static CheckCode { pub fn code(&self) -> &'static CheckCode {
match self { match self {
@ -64,37 +68,34 @@ impl CheckKind {
CheckKind::FStringMissingPlaceholders => &CheckCode::F541, CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
CheckKind::IfTuple => &CheckCode::F634, CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportStarUsage => &CheckCode::F403, CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::LineTooLong => &CheckCode::E501, CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::UnusedImport(_) => &CheckCode::F401,
} }
} }
/// The body text for the check. /// The body text for the check.
pub fn body(&self) -> &'static str { pub fn body(&self) -> String {
match self { match self {
CheckKind::DuplicateArgumentName => "Duplicate argument name in function definition", CheckKind::DuplicateArgumentName => {
CheckKind::FStringMissingPlaceholders => "f-string without any placeholders", "Duplicate argument name in function definition".to_string()
CheckKind::IfTuple => "If test is a tuple, which is always `True`", }
CheckKind::ImportStarUsage => "Unable to detect undefined names", CheckKind::FStringMissingPlaceholders => {
CheckKind::ReturnOutsideFunction => "a `return` statement outside of a function/method", "f-string without any placeholders".to_string()
}
CheckKind::IfTuple => {
"If test is a tuple.to_string(), which is always `True`".to_string()
}
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
CheckKind::LineTooLong => "Line too long".to_string(),
CheckKind::RaiseNotImplemented => { CheckKind::RaiseNotImplemented => {
"'raise NotImplemented' should be 'raise NotImplementedError" "'raise NotImplemented' should be 'raise NotImplementedError".to_string()
} }
CheckKind::LineTooLong => "Line too long", CheckKind::ReturnOutsideFunction => {
"a `return` statement outside of a function/method".to_string()
} }
} CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
/// The source for the checks (either the AST, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckKind::DuplicateArgumentName => &LintSource::AST,
CheckKind::FStringMissingPlaceholders => &LintSource::AST,
CheckKind::IfTuple => &LintSource::AST,
CheckKind::ImportStarUsage => &LintSource::AST,
CheckKind::ReturnOutsideFunction => &LintSource::AST,
CheckKind::RaiseNotImplemented => &LintSource::AST,
CheckKind::LineTooLong => &LintSource::Lines,
} }
} }
} }

View File

@ -6,7 +6,7 @@ use rustpython_parser::parser;
use crate::check_ast::check_ast; use crate::check_ast::check_ast;
use crate::check_lines::check_lines; use crate::check_lines::check_lines;
use crate::checks::{Check, CheckKind, LintSource}; use crate::checks::{Check, LintSource};
use crate::message::Message; use crate::message::Message;
use crate::settings::Settings; use crate::settings::Settings;
use crate::{cache, fs}; use crate::{cache, fs};
@ -28,7 +28,7 @@ pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Resul
if settings if settings
.select .select
.iter() .iter()
.any(|check_code| matches!(CheckKind::new(check_code).lint_source(), LintSource::AST)) .any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{ {
let python_ast = parser::parse_program(&contents, &path.to_string_lossy())?; let python_ast = parser::parse_program(&contents, &path.to_string_lossy())?;
checks.extend(check_ast(&python_ast, settings)); checks.extend(check_ast(&python_ast, settings));
@ -38,7 +38,7 @@ pub fn check_path(path: &Path, settings: &Settings, mode: &cache::Mode) -> Resul
if settings if settings
.select .select
.iter() .iter()
.any(|check_code| matches!(CheckKind::new(check_code).lint_source(), LintSource::Lines)) .any(|check_code| matches!(check_code.lint_source(), LintSource::Lines))
{ {
checks.extend(check_lines(&contents, settings)); checks.extend(check_lines(&contents, settings));
} }
@ -72,31 +72,50 @@ mod tests {
use crate::{cache, settings}; use crate::{cache, settings};
#[test] #[test]
fn duplicate_argument_name() -> Result<()> { fn e501() -> Result<()> {
let actual = check_path( let actual = check_path(
&Path::new("./resources/test/src/duplicate_argument_name.py"), &Path::new("./resources/test/src/E501.py"),
&settings::Settings { &settings::Settings {
line_length: 88, line_length: 88,
exclude: vec![], exclude: vec![],
select: BTreeSet::from([CheckCode::F831]), select: BTreeSet::from([CheckCode::E501]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::LineTooLong,
location: Location::new(5, 89),
filename: "./resources/test/src/E501.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f401() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F401]),
}, },
&cache::Mode::None, &cache::Mode::None,
)?; )?;
let expected = vec![ let expected = vec![
Message { Message {
kind: CheckKind::DuplicateArgumentName, kind: CheckKind::UnusedImport("functools".to_string()),
location: Location::new(1, 25), location: Location::new(2, 1),
filename: "./resources/test/src/duplicate_argument_name.py".to_string(), filename: "./resources/test/src/F401.py".to_string(),
}, },
Message { Message {
kind: CheckKind::DuplicateArgumentName, kind: CheckKind::UnusedImport("OrderedDict".to_string()),
location: Location::new(5, 28), location: Location::new(3, 1),
filename: "./resources/test/src/duplicate_argument_name.py".to_string(), filename: "./resources/test/src/F401.py".to_string(),
},
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(9, 27),
filename: "./resources/test/src/duplicate_argument_name.py".to_string(),
}, },
]; ];
assert_eq!(actual.len(), expected.len()); assert_eq!(actual.len(), expected.len());
@ -108,76 +127,9 @@ mod tests {
} }
#[test] #[test]
fn f_string_missing_placeholders() -> Result<()> { fn f403() -> Result<()> {
let actual = check_path( let actual = check_path(
&Path::new("./resources/test/src/f_string_missing_placeholders.py"), &Path::new("./resources/test/src/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(4, 7),
filename: "./resources/test/src/f_string_missing_placeholders.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(5, 7),
filename: "./resources/test/src/f_string_missing_placeholders.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(7, 7),
filename: "./resources/test/src/f_string_missing_placeholders.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn if_tuple() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/if_tuple.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::IfTuple,
location: Location::new(1, 1),
filename: "./resources/test/src/if_tuple.py".to_string(),
},
Message {
kind: CheckKind::IfTuple,
location: Location::new(7, 5),
filename: "./resources/test/src/if_tuple.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn import_star_usage() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/import_star_usage.py"),
&settings::Settings { &settings::Settings {
line_length: 88, line_length: 88,
exclude: vec![], exclude: vec![],
@ -189,12 +141,47 @@ mod tests {
Message { Message {
kind: CheckKind::ImportStarUsage, kind: CheckKind::ImportStarUsage,
location: Location::new(1, 1), location: Location::new(1, 1),
filename: "./resources/test/src/import_star_usage.py".to_string(), filename: "./resources/test/src/F403.py".to_string(),
}, },
Message { Message {
kind: CheckKind::ImportStarUsage, kind: CheckKind::ImportStarUsage,
location: Location::new(2, 1), location: Location::new(2, 1),
filename: "./resources/test/src/import_star_usage.py".to_string(), filename: "./resources/test/src/F403.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f541() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(4, 7),
filename: "./resources/test/src/F541.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(5, 7),
filename: "./resources/test/src/F541.py".to_string(),
},
Message {
kind: CheckKind::FStringMissingPlaceholders,
location: Location::new(7, 7),
filename: "./resources/test/src/F541.py".to_string(),
}, },
]; ];
assert_eq!(actual.len(), expected.len()); assert_eq!(actual.len(), expected.len());
@ -206,9 +193,40 @@ mod tests {
} }
#[test] #[test]
fn return_outside_function() -> Result<()> { fn f634() -> Result<()> {
let actual = check_path( let actual = check_path(
&Path::new("./resources/test/src/return_outside_function.py"), &Path::new("./resources/test/src/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::IfTuple,
location: Location::new(1, 1),
filename: "./resources/test/src/F634.py".to_string(),
},
Message {
kind: CheckKind::IfTuple,
location: Location::new(7, 5),
filename: "./resources/test/src/F634.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f706() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F706.py"),
&settings::Settings { &settings::Settings {
line_length: 88, line_length: 88,
exclude: vec![], exclude: vec![],
@ -220,12 +238,12 @@ mod tests {
Message { Message {
kind: CheckKind::ReturnOutsideFunction, kind: CheckKind::ReturnOutsideFunction,
location: Location::new(6, 5), location: Location::new(6, 5),
filename: "./resources/test/src/return_outside_function.py".to_string(), filename: "./resources/test/src/F706.py".to_string(),
}, },
Message { Message {
kind: CheckKind::ReturnOutsideFunction, kind: CheckKind::ReturnOutsideFunction,
location: Location::new(9, 1), location: Location::new(9, 1),
filename: "./resources/test/src/return_outside_function.py".to_string(), filename: "./resources/test/src/F706.py".to_string(),
}, },
]; ];
assert_eq!(actual.len(), expected.len()); assert_eq!(actual.len(), expected.len());
@ -237,9 +255,45 @@ mod tests {
} }
#[test] #[test]
fn raise_not_implemented() -> Result<()> { fn f831() -> Result<()> {
let actual = check_path( let actual = check_path(
&Path::new("./resources/test/src/raise_not_implemented.py"), &Path::new("./resources/test/src/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::F831]),
},
&cache::Mode::None,
)?;
let expected = vec![
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(1, 25),
filename: "./resources/test/src/F831.py".to_string(),
},
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(5, 28),
filename: "./resources/test/src/F831.py".to_string(),
},
Message {
kind: CheckKind::DuplicateArgumentName,
location: Location::new(9, 27),
filename: "./resources/test/src/F831.py".to_string(),
},
];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test]
fn f901() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/F901.py"),
&settings::Settings { &settings::Settings {
line_length: 88, line_length: 88,
exclude: vec![], exclude: vec![],
@ -251,12 +305,12 @@ mod tests {
Message { Message {
kind: CheckKind::RaiseNotImplemented, kind: CheckKind::RaiseNotImplemented,
location: Location::new(2, 5), location: Location::new(2, 5),
filename: "./resources/test/src/raise_not_implemented.py".to_string(), filename: "./resources/test/src/F901.py".to_string(),
}, },
Message { Message {
kind: CheckKind::RaiseNotImplemented, kind: CheckKind::RaiseNotImplemented,
location: Location::new(6, 5), location: Location::new(6, 5),
filename: "./resources/test/src/raise_not_implemented.py".to_string(), filename: "./resources/test/src/F901.py".to_string(),
}, },
]; ];
assert_eq!(actual.len(), expected.len()); assert_eq!(actual.len(), expected.len());
@ -266,28 +320,4 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn line_too_long() -> Result<()> {
let actual = check_path(
&Path::new("./resources/test/src/line_too_long.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E501]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::LineTooLong,
location: Location::new(5, 89),
filename: "./resources/test/src/line_too_long.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
} }

View File

@ -7,6 +7,7 @@ use anyhow::Result;
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::pyproject::load_config; use crate::pyproject::load_config;
#[derive(Debug)]
pub struct Settings { pub struct Settings {
pub line_length: usize, pub line_length: usize,
pub exclude: Vec<PathBuf>, pub exclude: Vec<PathBuf>,
@ -41,13 +42,14 @@ impl Settings {
.collect(), .collect(),
select: config.select.unwrap_or_else(|| { select: config.select.unwrap_or_else(|| {
BTreeSet::from([ BTreeSet::from([
CheckCode::F831, CheckCode::E501,
CheckCode::F401,
CheckCode::F403,
CheckCode::F541, CheckCode::F541,
CheckCode::F634, CheckCode::F634,
CheckCode::F403,
CheckCode::F706, CheckCode::F706,
CheckCode::F831,
CheckCode::F901, CheckCode::F901,
CheckCode::E501,
]) ])
}), }),
}) })