diff --git a/Cargo.lock b/Cargo.lock index 5801588c52..8fda27dc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.4" @@ -1482,6 +1491,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1507,6 +1533,7 @@ dependencies = [ "notify", "pyo3", "rayon", + "regex", "rustpython-parser", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 11f5f4743a..eed3799363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ colored = { version = "2.0.0" } fern = { version = "0.6.1" } log = { version = "0.4.17" } notify = { version = "4.0.17" } +pyo3 = { version = "0.16.5", features = ["extension-module", "abi3-py37"] } rayon = { version = "1.5.3" } +regex = { version = "1.6.0" } rustpython-parser = { git = "https://github.com/RustPython/RustPython.git", rev = "dff916d45c5d13074d21ad329a5ab68a6499426a" } serde = { version = "1.0.143", features = ["derive"] } serde_json = { version = "1.0.83" } walkdir = { version = "2.3.2" } -pyo3 = { version = "0.16.5", features = ["extension-module", "abi3-py37"] } diff --git a/resources/test/src/import_star_usage.py b/resources/test/src/import_star_usage.py index ebebd78484..5936ac786d 100644 --- a/resources/test/src/import_star_usage.py +++ b/resources/test/src/import_star_usage.py @@ -1 +1,6 @@ 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 diff --git a/src/check_ast.rs b/src/check_ast.rs index 4af031efd2..26a213f207 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -40,10 +40,10 @@ impl Visitor for Checker { fn visit_arguments(&mut self, arguments: &Arguments) { // Collect all the arguments into a single vector. let mut all_arguments: Vec<&Arg> = arguments - .posonlyargs + .args .iter() + .chain(arguments.posonlyargs.iter()) .chain(arguments.kwonlyargs.iter()) - .chain(arguments.args.iter()) .collect(); if let Some(arg) = &arguments.vararg { all_arguments.push(arg); diff --git a/src/check_lines.rs b/src/check_lines.rs index 86162f7851..85da502e93 100644 --- a/src/check_lines.rs +++ b/src/check_lines.rs @@ -2,16 +2,17 @@ use rustpython_parser::ast::Location; use crate::checks::Check; use crate::checks::CheckKind::LineTooLong; +use crate::settings::MAX_LINE_LENGTH; pub fn check_lines(contents: &str) -> Vec { contents .lines() .enumerate() .filter_map(|(row, line)| { - if line.len() > 79 { + if line.len() > *MAX_LINE_LENGTH { Some(Check { kind: LineTooLong, - location: Location::new(row + 1, 79 + 1), + location: Location::new(row + 1, MAX_LINE_LENGTH + 1), }) } else { None diff --git a/src/checks.rs b/src/checks.rs index 0746361319..bd278b9216 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -26,7 +26,7 @@ impl CheckKind { CheckKind::DuplicateArgumentName => "Duplicate argument name in function definition", CheckKind::IfTuple => "If test is a tuple, which is always `True`", CheckKind::ImportStarUsage => "Unable to detect undefined names", - CheckKind::LineTooLong => "Line too long (> 79 characters)", + CheckKind::LineTooLong => "Line too long", } } } diff --git a/src/fs.rs b/src/fs.rs index 096526741b..09d5eb0769 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,7 +1,8 @@ -use anyhow::Result; use std::fs::File; -use std::io::{BufReader, Read}; +use std::io::{BufRead, BufReader, Read}; use std::path::{Path, PathBuf}; + +use anyhow::Result; use walkdir::{DirEntry, WalkDir}; fn is_not_hidden(entry: &DirEntry) -> bool { @@ -21,6 +22,16 @@ pub fn iter_python_files(path: &PathBuf) -> impl Iterator { .filter(|entry| entry.path().to_string_lossy().ends_with(".py")) } +pub fn read_line(path: &Path, row: &usize) -> Result { + let file = File::open(path)?; + let buf_reader = BufReader::new(file); + buf_reader + .lines() + .nth(*row - 1) + .unwrap() + .map_err(|e| e.into()) +} + pub fn read_file(path: &Path) -> Result { let file = File::open(path)?; let mut buf_reader = BufReader::new(file); diff --git a/src/lib.rs b/src/lib.rs index fb6ce26356..e15e1a3d7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,4 +6,5 @@ pub mod fs; pub mod linter; pub mod logging; pub mod message; +mod settings; mod visitor; diff --git a/src/linter.rs b/src/linter.rs index 1429cc2933..89b2707d0f 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -29,6 +29,7 @@ pub fn check_path(path: &Path, mode: &cache::Mode) -> Result> { location: check.location, filename: path.to_string_lossy().to_string(), }) + .filter(|message| !message.is_inline_ignored()) .collect(); cache::set(path, &messages, mode); @@ -61,7 +62,7 @@ mod tests { }, Message { kind: DuplicateArgumentName, - location: Location::new(5, 9), + location: Location::new(5, 28), filename: "./resources/test/src/duplicate_argument_name.py".to_string(), }, Message { @@ -110,11 +111,18 @@ mod tests { &Path::new("./resources/test/src/import_star_usage.py"), &cache::Mode::None, )?; - let expected = vec![Message { - kind: ImportStarUsage, - location: Location::new(1, 1), - filename: "./resources/test/src/import_star_usage.py".to_string(), - }]; + let expected = vec![ + Message { + kind: ImportStarUsage, + location: Location::new(1, 1), + filename: "./resources/test/src/import_star_usage.py".to_string(), + }, + Message { + kind: ImportStarUsage, + location: Location::new(2, 1), + filename: "./resources/test/src/import_star_usage.py".to_string(), + }, + ]; assert_eq!(actual.len(), expected.len()); for i in 1..actual.len() { assert_eq!(actual[i], expected[i]); @@ -131,7 +139,7 @@ mod tests { )?; let expected = vec![Message { kind: LineTooLong, - location: Location::new(3, 80), + location: Location::new(3, 88), filename: "./resources/test/src/line_too_long.py".to_string(), }]; assert_eq!(actual.len(), expected.len()); diff --git a/src/message.rs b/src/message.rs index ce7f80ac5f..bce133c705 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,10 +1,13 @@ use std::fmt; +use std::path::Path; use colored::Colorize; +use regex::Regex; use rustpython_parser::ast::Location; use serde::{Deserialize, Serialize}; use crate::checks::CheckKind; +use crate::fs; #[derive(Serialize, Deserialize)] #[serde(remote = "Location")] @@ -29,6 +32,38 @@ pub struct Message { pub filename: String, } +impl Message { + pub fn is_inline_ignored(&self) -> bool { + match fs::read_line(Path::new(&self.filename), &self.location.row()) { + Ok(line) => { + // https://github.com/PyCQA/flake8/blob/799c71eeb61cf26c7c176aed43e22523e2a6d991/src/flake8/defaults.py#L26 + let re = Regex::new(r"(?i)# noqa(?::\s?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?") + .unwrap(); + match re.captures(&line) { + Some(caps) => match caps.name("codes") { + Some(codes) => { + let re = Regex::new(r"[,\s]").unwrap(); + for code in re + .split(codes.as_str()) + .map(|code| code.trim()) + .filter(|code| !code.is_empty()) + { + if code == self.kind.code() { + return true; + } + } + false + } + None => true, + }, + None => false, + } + } + Err(_) => false, + } + } +} + impl fmt::Display for Message { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000000..f21a4348f3 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1 @@ +pub static MAX_LINE_LENGTH: &usize = &88;