Programmers often leave comments to themselves and others such as:
# TODO: Use a faster algorithm?
The keywords used to prefix such comments are just a convention and vary
from project to project. Other common keywords include FIXME and HACK.
The keywords in use for the codebase are of interest to ruff because
ruff does also lint comments. For example the ERA lint detects
commented-out code but ignores comments starting with such a keyword.
Previously the ERA lint simply hardcoded the regular expression
TODO|FIXME|XXX to achieve that. This commit introduces a new `task-tags`
setting to make this configurable (and to allow other comment lints to
recognize the same set of keywords).
The term "task tags" has probably been popularized by the Eclipse
IDE.[1] For Python there has been the proposal PEP 350[2], which
referred to such keywords as "codetags". That proposal however has been
rejected. We are choosing the term "task tags" over "code tags" because
the former is more descriptive: a task tag describes a task.
While according to the PEP 350 such keywords are also sometimes used for
non-tasks e.g. NOBUG to describe a well-known problem that will never be
addressed due to design problems or domain limitations, such keywords
are so rare that we are neglecting them here in favor of more
descriptive terminology. The vast majority of such keywords does
describe tasks, so naming the setting "task-tags" is apt.
[1]: https://www.eclipse.org/pdt/help/html/task_tags.htm
[2]: https://peps.python.org/pep-0350/
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
245 lines
7.6 KiB
Rust
245 lines
7.6 KiB
Rust
use std::path::Path;
|
|
|
|
use rustpython_ast::Location;
|
|
use rustpython_parser::lexer::LexResult;
|
|
use serde::Serialize;
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
use crate::linter::check_path;
|
|
use crate::registry::CheckCode;
|
|
use crate::registry_gen::CheckCodePrefix;
|
|
use crate::rustpython_helpers::tokenize;
|
|
use crate::settings::configuration::Configuration;
|
|
use crate::settings::options::Options;
|
|
use crate::settings::types::PythonVersion;
|
|
use crate::settings::{flags, Settings};
|
|
use crate::source_code_locator::SourceCodeLocator;
|
|
use crate::source_code_style::SourceCodeStyleDetector;
|
|
use crate::{
|
|
directives, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_errmsg,
|
|
flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_tidy_imports,
|
|
flake8_unused_arguments, isort, mccabe, pep8_naming, pydocstyle, pyupgrade,
|
|
};
|
|
|
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
#[wasm_bindgen(typescript_custom_section)]
|
|
const TYPES: &'static str = r#"
|
|
export interface Check {
|
|
code: string;
|
|
message: string;
|
|
location: {
|
|
row: number;
|
|
column: number;
|
|
};
|
|
end_location: {
|
|
row: number;
|
|
column: number;
|
|
};
|
|
fix: {
|
|
content: string;
|
|
message: string | null;
|
|
location: {
|
|
row: number;
|
|
column: number;
|
|
};
|
|
end_location: {
|
|
row: number;
|
|
column: number;
|
|
};
|
|
} | null;
|
|
};
|
|
"#;
|
|
|
|
#[derive(Serialize)]
|
|
struct ExpandedMessage {
|
|
code: CheckCode,
|
|
message: String,
|
|
location: Location,
|
|
end_location: Location,
|
|
fix: Option<ExpandedFix>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ExpandedFix {
|
|
content: String,
|
|
message: Option<String>,
|
|
location: Location,
|
|
end_location: Location,
|
|
}
|
|
|
|
#[wasm_bindgen(start)]
|
|
pub fn run() {
|
|
use log::Level;
|
|
console_error_panic_hook::set_once();
|
|
console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong.");
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
#[allow(non_snake_case)]
|
|
pub fn currentVersion() -> JsValue {
|
|
JsValue::from(VERSION)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
#[allow(non_snake_case)]
|
|
pub fn defaultSettings() -> Result<JsValue, JsValue> {
|
|
Ok(serde_wasm_bindgen::to_value(&Options {
|
|
// Propagate defaults.
|
|
allowed_confusables: Some(Vec::default()),
|
|
dummy_variable_rgx: Some("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$".to_string()),
|
|
extend_ignore: Some(Vec::default()),
|
|
extend_select: Some(Vec::default()),
|
|
external: Some(Vec::default()),
|
|
ignore: Some(Vec::default()),
|
|
line_length: Some(88),
|
|
select: Some(vec![CheckCodePrefix::E, CheckCodePrefix::F]),
|
|
target_version: Some(PythonVersion::default()),
|
|
// Ignore a bunch of options that don't make sense in a single-file editor.
|
|
cache_dir: None,
|
|
exclude: None,
|
|
extend: None,
|
|
extend_exclude: None,
|
|
fix: None,
|
|
fix_only: None,
|
|
fixable: None,
|
|
force_exclude: None,
|
|
format: None,
|
|
ignore_init_module_imports: None,
|
|
per_file_ignores: None,
|
|
required_version: None,
|
|
respect_gitignore: None,
|
|
show_source: None,
|
|
src: None,
|
|
unfixable: None,
|
|
task_tags: None,
|
|
update_check: None,
|
|
// Use default options for all plugins.
|
|
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
|
|
flake8_bandit: Some(flake8_bandit::settings::Settings::default().into()),
|
|
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
|
|
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
|
|
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
|
|
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
|
|
flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()),
|
|
flake8_import_conventions: Some(
|
|
flake8_import_conventions::settings::Settings::default().into(),
|
|
),
|
|
flake8_unused_arguments: Some(
|
|
flake8_unused_arguments::settings::Settings::default().into(),
|
|
),
|
|
isort: Some(isort::settings::Settings::default().into()),
|
|
mccabe: Some(mccabe::settings::Settings::default().into()),
|
|
pep8_naming: Some(pep8_naming::settings::Settings::default().into()),
|
|
pydocstyle: Some(pydocstyle::settings::Settings::default().into()),
|
|
pyupgrade: Some(pyupgrade::settings::Settings::default().into()),
|
|
})?)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
#[allow(non_snake_case)]
|
|
pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
|
|
let options: Options = serde_wasm_bindgen::from_value(options).map_err(|e| e.to_string())?;
|
|
let configuration =
|
|
Configuration::from_options(options, Path::new(".")).map_err(|e| e.to_string())?;
|
|
let settings =
|
|
Settings::from_configuration(configuration, Path::new(".")).map_err(|e| e.to_string())?;
|
|
|
|
// Tokenize once.
|
|
let tokens: Vec<LexResult> = tokenize(contents);
|
|
|
|
// Map row and column locations to byte slices (lazily).
|
|
let locator = SourceCodeLocator::new(contents);
|
|
|
|
// Detect the current code style (lazily).
|
|
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
|
|
|
|
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
|
let directives = directives::extract_directives(&tokens, &locator, directives::Flags::empty());
|
|
|
|
// Generate checks.
|
|
let checks = check_path(
|
|
Path::new("<filename>"),
|
|
None,
|
|
contents,
|
|
tokens,
|
|
&locator,
|
|
&stylist,
|
|
&directives,
|
|
&settings,
|
|
flags::Autofix::Enabled,
|
|
flags::Noqa::Enabled,
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let messages: Vec<ExpandedMessage> = checks
|
|
.into_iter()
|
|
.map(|check| ExpandedMessage {
|
|
code: check.kind.code().clone(),
|
|
message: check.kind.body(),
|
|
location: check.location,
|
|
end_location: check.end_location,
|
|
fix: check.fix.map(|fix| ExpandedFix {
|
|
content: fix.content,
|
|
message: check.kind.commit(),
|
|
location: fix.location,
|
|
end_location: fix.end_location,
|
|
}),
|
|
})
|
|
.collect();
|
|
|
|
Ok(serde_wasm_bindgen::to_value(&messages)?)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use js_sys;
|
|
use wasm_bindgen_test::*;
|
|
|
|
use super::*;
|
|
|
|
macro_rules! check {
|
|
($source:expr, $config:expr, $expected:expr) => {{
|
|
let foo = js_sys::JSON::parse($config).unwrap();
|
|
match check($source, foo) {
|
|
Ok(output) => {
|
|
let result: Vec<Message> = serde_wasm_bindgen::from_value(output).unwrap();
|
|
assert_eq!(result, $expected);
|
|
}
|
|
Err(e) => assert!(false, "{:#?}", e),
|
|
}
|
|
}};
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn empty_config() {
|
|
check!(
|
|
"if (1, 2): pass",
|
|
r#"{}"#,
|
|
[ExpandedMessage {
|
|
code: CheckCode::F634,
|
|
message: "If test is a tuple, which is always `True`".to_string(),
|
|
location: Location::new(1, 0),
|
|
end_location: Location::new(1, 15),
|
|
fix: None,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn partial_config() {
|
|
check!("if (1, 2): pass", r#"{"ignore": ["F"]}"#, []);
|
|
}
|
|
|
|
#[wasm_bindgen_test]
|
|
fn partial_nested_config() {
|
|
let config = r#"{
|
|
"select": ["Q"],
|
|
"flake8-quotes": {
|
|
"inline-quotes": "single"
|
|
}
|
|
}"#;
|
|
check!(r#"print('hello world')"#, config, []);
|
|
}
|
|
}
|