Implement E402 (ModuleImportNotAtTopOfFile) (#102)

This commit is contained in:
Charlie Marsh 2022-09-04 16:20:36 -04:00 committed by GitHub
parent 533b4e752b
commit 3cf9e3b201
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 16 deletions

View file

@ -110,6 +110,7 @@ OPTIONS:
| Code | Name | Message | | Code | Name | Message |
| ---- | ----- | ------- | | ---- | ----- | ------- |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file |
| E501 | LineTooLong | Line too long | | E501 | LineTooLong | Line too long |
| F401 | UnusedImport | `...` imported but unused | | F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | Unable to detect undefined names | | F403 | ImportStarUsage | Unable to detect undefined names |

View file

@ -10,6 +10,7 @@ fn main() {
CheckKind::IfTuple, CheckKind::IfTuple,
CheckKind::ImportStarUsage, CheckKind::ImportStarUsage,
CheckKind::LineTooLong, CheckKind::LineTooLong,
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::RaiseNotImplemented, CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction, CheckKind::ReturnOutsideFunction,
CheckKind::UndefinedExport("...".to_string()), CheckKind::UndefinedExport("...".to_string()),

28
resources/test/fixtures/E402.py vendored Normal file
View file

@ -0,0 +1,28 @@
"""Top-level docstring."""
import a
try:
import b
except ImportError:
pass
else:
pass
import c
if x > 0:
import d
else:
import e
y = x + 1
import f
def foo() -> None:
import e
if __name__ == "__main__":
import g

View file

@ -2,6 +2,7 @@
line-length = 88 line-length = 88
exclude = ["excluded.py", "**/migrations"] exclude = ["excluded.py", "**/migrations"]
select = [ select = [
"E402",
"E501", "E501",
"F401", "F401",
"F403", "F403",

View file

@ -23,6 +23,8 @@ struct Checker<'a> {
deferred: Vec<String>, deferred: Vec<String>,
in_f_string: bool, in_f_string: bool,
in_annotation: bool, in_annotation: bool,
seen_non_import: bool,
seen_docstring: bool,
} }
impl Checker<'_> { impl Checker<'_> {
@ -36,6 +38,8 @@ impl Checker<'_> {
deferred: vec![], deferred: vec![],
in_f_string: false, in_f_string: false,
in_annotation: false, in_annotation: false,
seen_non_import: false,
seen_docstring: false,
} }
} }
} }
@ -154,6 +158,19 @@ impl Visitor for Checker<'_> {
self.push_scope(Scope::new(ScopeKind::Class)) self.push_scope(Scope::new(ScopeKind::Class))
} }
StmtKind::Import { names } => { StmtKind::Import { names } => {
if self
.settings
.select
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& stmt.location.column() == 1
{
self.checks.push(Check {
kind: CheckKind::ModuleImportNotAtTopOfFile,
location: stmt.location,
});
}
for alias in names { for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() { if alias.node.name.contains('.') && alias.node.asname.is_none() {
// TODO(charlie): Multiple submodule imports with the same parent module // TODO(charlie): Multiple submodule imports with the same parent module
@ -191,6 +208,19 @@ impl Visitor for Checker<'_> {
} }
} }
StmtKind::ImportFrom { names, module, .. } => { StmtKind::ImportFrom { names, module, .. } => {
if self
.settings
.select
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& stmt.location.column() == 1
{
self.checks.push(Check {
kind: CheckKind::ModuleImportNotAtTopOfFile,
location: stmt.location,
});
}
for alias in names { for alias in names {
let name = alias let name = alias
.node .node
@ -282,8 +312,12 @@ impl Visitor for Checker<'_> {
} }
} }
} }
StmtKind::AugAssign { target, .. } => self.handle_node_load(target), StmtKind::AugAssign { target, .. } => {
self.seen_non_import = true;
self.handle_node_load(target);
}
StmtKind::Assert { test, .. } => { StmtKind::Assert { test, .. } => {
self.seen_non_import = true;
if self.settings.select.contains(CheckKind::AssertTuple.code()) { if self.settings.select.contains(CheckKind::AssertTuple.code()) {
if let ExprKind::Tuple { elts, .. } = &test.node { if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() { if !elts.is_empty() {
@ -312,6 +346,22 @@ impl Visitor for Checker<'_> {
} }
} }
} }
StmtKind::Expr { value } => {
if !self.seen_docstring {
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &value.node
{
self.seen_docstring = true;
}
} else {
self.seen_non_import = true;
}
}
StmtKind::Delete { .. } | StmtKind::Assign { .. } | StmtKind::AnnAssign { .. } => {
self.seen_non_import = true;
}
_ => {} _ => {}
} }

View file

@ -8,6 +8,7 @@ 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 {
E402,
E501, E501,
F401, F401,
F403, F403,
@ -31,6 +32,7 @@ impl FromStr for CheckCode {
fn from_str(s: &str) -> Result<Self> { fn from_str(s: &str) -> Result<Self> {
match s { match s {
"E402" => Ok(CheckCode::E402),
"E501" => Ok(CheckCode::E501), "E501" => Ok(CheckCode::E501),
"F401" => Ok(CheckCode::F401), "F401" => Ok(CheckCode::F401),
"F403" => Ok(CheckCode::F403), "F403" => Ok(CheckCode::F403),
@ -55,6 +57,7 @@ impl FromStr for CheckCode {
impl CheckCode { impl CheckCode {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
CheckCode::E402 => "E402",
CheckCode::E501 => "E501", CheckCode::E501 => "E501",
CheckCode::F401 => "F401", CheckCode::F401 => "F401",
CheckCode::F403 => "F403", CheckCode::F403 => "F403",
@ -77,6 +80,7 @@ impl CheckCode {
/// The source for the check (either the AST, or the physical lines). /// The source for the check (either the AST, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource { pub fn lint_source(&self) -> &'static LintSource {
match self { match self {
CheckCode::E402 => &LintSource::AST,
CheckCode::E501 => &LintSource::Lines, CheckCode::E501 => &LintSource::Lines,
CheckCode::F401 => &LintSource::AST, CheckCode::F401 => &LintSource::AST,
CheckCode::F403 => &LintSource::AST, CheckCode::F403 => &LintSource::AST,
@ -105,17 +109,18 @@ pub enum LintSource {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind { pub enum CheckKind {
AssertTuple,
DefaultExceptNotLast,
DuplicateArgumentName, DuplicateArgumentName,
FStringMissingPlaceholders, FStringMissingPlaceholders,
AssertTuple,
IfTuple, IfTuple,
ImportStarUsage, ImportStarUsage,
LineTooLong, LineTooLong,
ModuleImportNotAtTopOfFile,
RaiseNotImplemented, RaiseNotImplemented,
ReturnOutsideFunction, ReturnOutsideFunction,
DefaultExceptNotLast,
UndefinedLocal(String),
UndefinedExport(String), UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String), UndefinedName(String),
UnusedImport(String), UnusedImport(String),
UnusedVariable(String), UnusedVariable(String),
@ -127,17 +132,18 @@ impl CheckKind {
/// The name of the check. /// The name of the check.
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
CheckKind::AssertTuple => "AssertTuple",
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
CheckKind::DuplicateArgumentName => "DuplicateArgumentName", CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders", CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
CheckKind::AssertTuple => "AssertTuple",
CheckKind::IfTuple => "IfTuple", CheckKind::IfTuple => "IfTuple",
CheckKind::ImportStarUsage => "ImportStarUsage", CheckKind::ImportStarUsage => "ImportStarUsage",
CheckKind::LineTooLong => "LineTooLong", CheckKind::LineTooLong => "LineTooLong",
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
CheckKind::RaiseNotImplemented => "RaiseNotImplemented", CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction", CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
CheckKind::UndefinedExport(_) => "UndefinedExport", CheckKind::UndefinedExport(_) => "UndefinedExport",
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
CheckKind::UndefinedName(_) => "UndefinedName", CheckKind::UndefinedName(_) => "UndefinedName",
CheckKind::UnusedImport(_) => "UnusedImport", CheckKind::UnusedImport(_) => "UnusedImport",
CheckKind::UnusedVariable(_) => "UnusedVariable", CheckKind::UnusedVariable(_) => "UnusedVariable",
@ -149,15 +155,16 @@ impl CheckKind {
/// 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 {
CheckKind::AssertTuple => &CheckCode::F631,
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
CheckKind::DuplicateArgumentName => &CheckCode::F831, CheckKind::DuplicateArgumentName => &CheckCode::F831,
CheckKind::FStringMissingPlaceholders => &CheckCode::F541, CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
CheckKind::AssertTuple => &CheckCode::F631,
CheckKind::IfTuple => &CheckCode::F634, CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportStarUsage => &CheckCode::F403, CheckKind::ImportStarUsage => &CheckCode::F403,
CheckKind::LineTooLong => &CheckCode::E501, CheckKind::LineTooLong => &CheckCode::E501,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::RaiseNotImplemented => &CheckCode::F901, CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706, CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
CheckKind::UndefinedExport(_) => &CheckCode::F822, CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823, CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821, CheckKind::UndefinedName(_) => &CheckCode::F821,
@ -171,27 +178,30 @@ impl CheckKind {
/// The body text for the check. /// The body text for the check.
pub fn body(&self) -> String { pub fn body(&self) -> String {
match self { match self {
CheckKind::AssertTuple => {
"Assert test is a non-empty tuple, which is always `True`".to_string()
}
CheckKind::DefaultExceptNotLast => {
"an `except:` block as not the last exception handler".to_string()
}
CheckKind::DuplicateArgumentName => { CheckKind::DuplicateArgumentName => {
"Duplicate argument name in function definition".to_string() "Duplicate argument name in function definition".to_string()
} }
CheckKind::FStringMissingPlaceholders => { CheckKind::FStringMissingPlaceholders => {
"f-string without any placeholders".to_string() "f-string without any placeholders".to_string()
} }
CheckKind::AssertTuple => {
"Assert test is a non-empty tuple, which is always `True`".to_string()
}
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(), CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(), CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
CheckKind::LineTooLong => "Line too long".to_string(), CheckKind::LineTooLong => "Line too long".to_string(),
CheckKind::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::RaiseNotImplemented => { CheckKind::RaiseNotImplemented => {
"`raise NotImplemented` should be `raise NotImplementedError`".to_string() "`raise NotImplemented` should be `raise NotImplementedError`".to_string()
} }
CheckKind::ReturnOutsideFunction => { CheckKind::ReturnOutsideFunction => {
"a `return` statement outside of a function/method".to_string() "a `return` statement outside of a function/method".to_string()
} }
CheckKind::DefaultExceptNotLast => {
"an `except:` block as not the last exception handler".to_string()
}
CheckKind::UndefinedExport(name) => { CheckKind::UndefinedExport(name) => {
format!("Undefined name `{name}` in `__all__`") format!("Undefined name `{name}` in `__all__`")
} }

View file

@ -65,6 +65,30 @@ mod tests {
use crate::message::Message; use crate::message::Message;
use crate::{cache, settings}; use crate::{cache, settings};
#[test]
fn e402() -> Result<()> {
let actual = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
select: BTreeSet::from([CheckCode::E402]),
},
&cache::Mode::None,
)?;
let expected = vec![Message {
kind: CheckKind::ModuleImportNotAtTopOfFile,
location: Location::new(20, 1),
filename: "./resources/test/fixtures/E402.py".to_string(),
}];
assert_eq!(actual.len(), expected.len());
for i in 0..actual.len() {
assert_eq!(actual[i], expected[i]);
}
Ok(())
}
#[test] #[test]
fn e501() -> Result<()> { fn e501() -> Result<()> {
let actual = check_path( let actual = check_path(

View file

@ -237,6 +237,7 @@ other-attribute = 1
Path::new("**/migrations").to_path_buf() Path::new("**/migrations").to_path_buf()
]), ]),
select: Some(BTreeSet::from([ select: Some(BTreeSet::from([
CheckCode::E402,
CheckCode::E501, CheckCode::E501,
CheckCode::F401, CheckCode::F401,
CheckCode::F403, CheckCode::F403,

View file

@ -44,6 +44,7 @@ impl Settings {
.collect(), .collect(),
select: config.select.unwrap_or_else(|| { select: config.select.unwrap_or_else(|| {
BTreeSet::from([ BTreeSet::from([
CheckCode::E402,
CheckCode::E501, CheckCode::E501,
CheckCode::F401, CheckCode::F401,
CheckCode::F403, CheckCode::F403,
@ -52,8 +53,8 @@ impl Settings {
CheckCode::F634, CheckCode::F634,
CheckCode::F706, CheckCode::F706,
CheckCode::F707, CheckCode::F707,
CheckCode::F831,
CheckCode::F823, CheckCode::F823,
CheckCode::F831,
CheckCode::F901, CheckCode::F901,
]) ])
}), }),