mirror of https://github.com/astral-sh/ruff
Implement `flake8_todos` (#3921)
This commit is contained in:
parent
7e7be05ddf
commit
2f53781a77
31
LICENSE
31
LICENSE
|
|
@ -354,6 +354,37 @@ are:
|
|||
SOFTWARE.
|
||||
"""
|
||||
|
||||
- flake8-todos, licensed as follows:
|
||||
"""
|
||||
Copyright (c) 2019 EclecticIQ. All rights reserved.
|
||||
Copyright (c) 2020 Gram <gram@orsinium.dev>. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
- flake8-unused-arguments, licensed as follows:
|
||||
"""
|
||||
MIT License
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ quality tools, including:
|
|||
- [flake8-simplify](https://pypi.org/project/flake8-simplify/)
|
||||
- [flake8-super](https://pypi.org/project/flake8-super/)
|
||||
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
|
||||
- [flake8-todos](https://pypi.org/project/flake8-todos/)
|
||||
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
|
||||
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/charliermarsh/ruff/issues/2102))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# T001 - accepted
|
||||
# TODO (evanrittenhouse): this is a valid TODO
|
||||
# SOME_OTHER_TAG: this is impossible to determine
|
||||
# this is not a TODO
|
||||
|
||||
# T001 - errors
|
||||
# XXX (evanrittenhouse): this is not fine
|
||||
# FIXME (evanrittenhouse): this is not fine
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# T002 - accepted
|
||||
# TODO (evanrittenhouse): this has an author
|
||||
# TODO(evanrittenhouse): this also has an author
|
||||
# T002 - errors
|
||||
# TODO: this has no author
|
||||
# FIXME: neither does this
|
||||
# TODO : and neither does this
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# TDO003 - accepted
|
||||
# TODO: this comment has a link
|
||||
# https://github.com/charliermarsh/ruff/issues/3870
|
||||
|
||||
# TODO: this comment has an issue
|
||||
# TDO-3870
|
||||
|
||||
# TDO003 - errors
|
||||
# TODO: this comment has no
|
||||
# link after it
|
||||
|
||||
# TODO: here's a TODO with no link after it
|
||||
def foo(x):
|
||||
return x
|
||||
|
||||
# TODO: here's a TODO on the last line with no link
|
||||
# Here's more content.
|
||||
# TDO-3870
|
||||
|
||||
# TODO: here's a TODO on the last line with no link
|
||||
# Here's more content, with a space.
|
||||
|
||||
# TDO-3870
|
||||
|
||||
# TODO: here's a TODO without an issue link
|
||||
# TODO: followed by a new TODO with an issue link
|
||||
# TDO-3870
|
||||
|
||||
# TODO: here's a TODO on the last line with no link
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# T004 - accepted
|
||||
# TODO(evanrittenhouse): this has a colon
|
||||
# T004 - errors
|
||||
# TODO this has no colon
|
||||
# TODO(evanrittenhouse 😀) this has no colon
|
||||
# FIXME add a colon
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# T005 - accepted
|
||||
# TODO(evanrittenhouse): this has text, while the errors do not
|
||||
# T005 - errors
|
||||
# TODO(evanrittenhouse):
|
||||
# TODO(evanrittenhouse)
|
||||
# FIXME
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# TDO006 - accepted
|
||||
# TODO (evanrittenhouse): this is a valid TODO
|
||||
# TDO006 - error
|
||||
# ToDo (evanrittenhouse): invalid capitalization
|
||||
# todo (evanrittenhouse): another invalid capitalization
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# T007 - accepted
|
||||
# TODO(evanrittenhouse): this has a space after a colon
|
||||
# TODO: so does this
|
||||
# T007 - errors
|
||||
# TODO(evanrittenhouse):this has no space after a colon
|
||||
# TODO (evanrittenhouse):this doesn't either
|
||||
# TODO:neither does this
|
||||
# FIXME:and lastly neither does this
|
||||
|
|
@ -7,8 +7,8 @@ use crate::lex::docstring_detection::StateMachine;
|
|||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::ruff::rules::Context;
|
||||
use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, pycodestyle,
|
||||
pylint, pyupgrade, ruff,
|
||||
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, flake8_todos,
|
||||
pycodestyle, pylint, pyupgrade, ruff,
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
|
|
@ -59,6 +59,15 @@ pub(crate) fn check_tokens(
|
|||
]);
|
||||
let enforce_extraneous_parenthesis = settings.rules.enabled(Rule::ExtraneousParentheses);
|
||||
let enforce_type_comment_in_stub = settings.rules.enabled(Rule::TypeCommentInStub);
|
||||
let enforce_todos = settings.rules.any_enabled(&[
|
||||
Rule::InvalidTodoTag,
|
||||
Rule::MissingTodoAuthor,
|
||||
Rule::MissingTodoLink,
|
||||
Rule::MissingTodoColon,
|
||||
Rule::MissingTodoDescription,
|
||||
Rule::InvalidTodoCapitalization,
|
||||
Rule::MissingSpaceAfterTodoColon,
|
||||
]);
|
||||
|
||||
// RUF001, RUF002, RUF003
|
||||
if enforce_ambiguous_unicode_character {
|
||||
|
|
@ -179,5 +188,14 @@ pub(crate) fn check_tokens(
|
|||
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(tokens));
|
||||
}
|
||||
|
||||
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
|
||||
if enforce_todos {
|
||||
diagnostics.extend(
|
||||
flake8_todos::rules::todos(tokens, settings)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
|
|
|||
|
|
@ -743,6 +743,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
|||
// Reserved: (Flynt, "001") => Rule::StringConcatenationToFString,
|
||||
(Flynt, "002") => Rule::StaticJoinToFString,
|
||||
|
||||
// flake8-todo
|
||||
(Flake8Todo, "001") => Rule::InvalidTodoTag,
|
||||
(Flake8Todo, "002") => Rule::MissingTodoAuthor,
|
||||
(Flake8Todo, "003") => Rule::MissingTodoLink,
|
||||
(Flake8Todo, "004") => Rule::MissingTodoColon,
|
||||
(Flake8Todo, "005") => Rule::MissingTodoDescription,
|
||||
(Flake8Todo, "006") => Rule::InvalidTodoCapitalization,
|
||||
(Flake8Todo, "007") => Rule::MissingSpaceAfterTodoColon,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -674,6 +674,14 @@ ruff_macros::register_rules!(
|
|||
rules::flake8_django::rules::DjangoNonLeadingReceiverDecorator,
|
||||
// flynt
|
||||
rules::flynt::rules::StaticJoinToFString,
|
||||
// flake8-todo
|
||||
rules::flake8_todos::rules::InvalidTodoTag,
|
||||
rules::flake8_todos::rules::MissingTodoAuthor,
|
||||
rules::flake8_todos::rules::MissingTodoLink,
|
||||
rules::flake8_todos::rules::MissingTodoColon,
|
||||
rules::flake8_todos::rules::MissingTodoDescription,
|
||||
rules::flake8_todos::rules::InvalidTodoCapitalization,
|
||||
rules::flake8_todos::rules::MissingSpaceAfterTodoColon,
|
||||
);
|
||||
|
||||
pub trait AsRule {
|
||||
|
|
@ -814,6 +822,9 @@ pub enum Linter {
|
|||
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
#[prefix = "PTH"]
|
||||
Flake8UsePathlib,
|
||||
/// [flake8-todos](https://github.com/orsinium-labs/flake8-todos/)
|
||||
#[prefix = "TD"]
|
||||
Flake8Todo,
|
||||
/// [eradicate](https://pypi.org/project/eradicate/)
|
||||
#[prefix = "ERA"]
|
||||
Eradicate,
|
||||
|
|
@ -938,7 +949,14 @@ impl Rule {
|
|||
| Rule::UselessSemicolon
|
||||
| Rule::MultipleStatementsOnOneLineSemicolon
|
||||
| Rule::ProhibitedTrailingComma
|
||||
| Rule::TypeCommentInStub => LintSource::Tokens,
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::InvalidTodoTag
|
||||
| Rule::MissingTodoAuthor
|
||||
| Rule::MissingTodoLink
|
||||
| Rule::MissingTodoColon
|
||||
| Rule::MissingTodoDescription
|
||||
| Rule::InvalidTodoCapitalization
|
||||
| Rule::MissingSpaceAfterTodoColon => LintSource::Tokens,
|
||||
Rule::IOError => LintSource::Io,
|
||||
Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports,
|
||||
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::AsRef;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::InvalidTodoTag, Path::new("TD001.py"); "TD001")]
|
||||
#[test_case(Rule::MissingTodoAuthor, Path::new("TD002.py"); "TD002")]
|
||||
#[test_case(Rule::MissingTodoLink, Path::new("TD003.py"); "TD003")]
|
||||
#[test_case(Rule::MissingTodoColon, Path::new("TD004.py"); "TD004")]
|
||||
#[test_case(Rule::MissingTodoDescription, Path::new("TD005.py"); "TD005")]
|
||||
#[test_case(Rule::InvalidTodoCapitalization, Path::new("TD006.py"); "TD006")]
|
||||
#[test_case(Rule::MissingSpaceAfterTodoColon, Path::new("TD007.py"); "TD007")]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_todos").join(path).as_path(),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::RegexSet;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::{registry::Rule, settings::Settings};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a TODO comment is labelled with "TODO".
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Ambiguous tags reduce code visibility and can lead to dangling TODOs.
|
||||
/// For example, if a comment is tagged with "FIXME" rather than "TODO", it may
|
||||
/// be overlooked by future readers.
|
||||
///
|
||||
/// Note that this rule will only flag "FIXME" and "XXX" tags as incorrect.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # FIXME(ruff): this should get fixed!
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # TODO(ruff): this is now fixed!
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct InvalidTodoTag {
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
impl Violation for InvalidTodoTag {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let InvalidTodoTag { tag } = self;
|
||||
format!("Invalid TODO tag: `{tag}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a TODO comment includes an author.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Including an author on a TODO provides future readers with context around
|
||||
/// the issue. While the TODO author is not always considered responsible for
|
||||
/// fixing the issue, they are typically the individual with the most context.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # TODO: should assign an author here
|
||||
/// ```
|
||||
///
|
||||
/// Use instead
|
||||
/// ```python
|
||||
/// # TODO(charlie): now an author is assigned
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTodoAuthor;
|
||||
|
||||
impl Violation for MissingTodoAuthor {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing author in TODO; try: `# TODO(<author_name>): ...`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a TODO comment is associated with a link to a relevant issue
|
||||
/// or ticket.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Including an issue link near a TODO makes it easier for resolvers
|
||||
/// to get context around the issue.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # TODO: this link has no issue
|
||||
/// ```
|
||||
///
|
||||
/// Use one of these instead:
|
||||
/// ```python
|
||||
/// # TODO(charlie): this comment has an issue link
|
||||
/// # https://github.com/charliermarsh/ruff/issues/3870
|
||||
///
|
||||
/// # TODO(charlie): this comment has a 3-digit issue code
|
||||
/// # 003
|
||||
///
|
||||
/// # TODO(charlie): this comment has an issue code of (up to) 6 characters, then digits
|
||||
/// # SIXCHR-003
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTodoLink;
|
||||
|
||||
impl Violation for MissingTodoLink {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing issue link on the line following this TODO")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a "TODO" tag is followed by a colon.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "TODO" tags are typically followed by a parenthesized author name, a colon,
|
||||
/// a space, and a description of the issue, in that order.
|
||||
///
|
||||
/// Deviating from this pattern can lead to inconsistent and non-idiomatic
|
||||
/// comments.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # TODO(charlie) fix this colon
|
||||
/// ```
|
||||
///
|
||||
/// Used instead:
|
||||
/// ```python
|
||||
/// # TODO(charlie): colon fixed
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTodoColon;
|
||||
|
||||
impl Violation for MissingTodoColon {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing colon in TODO")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a "TODO" tag contains a description of the issue following the
|
||||
/// tag itself.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// TODO comments should include a description of the issue to provide context
|
||||
/// for future readers.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # TODO(charlie)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # TODO(charlie): fix some issue
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTodoDescription;
|
||||
|
||||
impl Violation for MissingTodoDescription {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing issue description after `TODO`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that a "TODO" tag is properly capitalized (i.e., that the tag is
|
||||
/// uppercase).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Capitalizing the "TODO" in a TODO comment is a convention that makes it
|
||||
/// easier for future readers to identify TODOs.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # todo(charlie): capitalize this
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # TODO(charlie): this is capitalized
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct InvalidTodoCapitalization {
|
||||
tag: String,
|
||||
}
|
||||
|
||||
impl AlwaysAutofixableViolation for InvalidTodoCapitalization {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let InvalidTodoCapitalization { tag } = self;
|
||||
format!("Invalid TODO capitalization: `{tag}` should be `TODO`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let InvalidTodoCapitalization { tag } = self;
|
||||
format!("Replace `{tag}` with `TODO`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that the colon after a "TODO" tag is followed by a space.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "TODO" tags are typically followed by a parenthesized author name, a colon,
|
||||
/// a space, and a description of the issue, in that order.
|
||||
///
|
||||
/// Deviating from this pattern can lead to inconsistent and non-idiomatic
|
||||
/// comments.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # TODO(charlie):fix this
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # TODO(charlie): fix this
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingSpaceAfterTodoColon;
|
||||
|
||||
impl Violation for MissingSpaceAfterTodoColon {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Missing space after colon in TODO")
|
||||
}
|
||||
}
|
||||
|
||||
static TODO_REGEX_SET: Lazy<RegexSet> = Lazy::new(|| {
|
||||
RegexSet::new([
|
||||
r#"^#\s*(?i)(TODO).*$"#,
|
||||
r#"^#\s*(?i)(FIXME).*$"#,
|
||||
r#"^#\s*(?i)(XXX).*$"#,
|
||||
])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
// Maps the index of a particular Regex (specified by its index in the above TODO_REGEX_SET slice)
|
||||
// to the length of the tag that we're trying to capture.
|
||||
static PATTERN_TAG_LENGTH: &[usize; 3] = &["TODO".len(), "FIXME".len(), "XXX".len()];
|
||||
|
||||
static ISSUE_LINK_REGEX_SET: Lazy<RegexSet> = Lazy::new(|| {
|
||||
let patterns: [&str; 3] = [
|
||||
r#"^#\s*(http|https)://.*"#, // issue link
|
||||
r#"^#\s*\d+$"#, // issue code - like "003"
|
||||
r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" or "TD-003"
|
||||
];
|
||||
RegexSet::new(patterns).unwrap()
|
||||
});
|
||||
|
||||
// If this struct ever gets pushed outside of this module, it may be worth creating an enum for
|
||||
// the different tag types + other convenience methods.
|
||||
/// Represents a TODO tag or any of its variants - FIXME, XXX, TODO.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Tag<'a> {
|
||||
range: TextRange,
|
||||
content: &'a str,
|
||||
}
|
||||
|
||||
pub(crate) fn todos(tokens: &[LexResult], settings: &Settings) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
let mut iter = tokens.iter().flatten().multipeek();
|
||||
while let Some((token, token_range)) = iter.next() {
|
||||
let Tok::Comment(comment) = token else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Check that the comment is a TODO (properly formed or not).
|
||||
let Some(tag) = detect_tag(comment, token_range) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
tag_errors(&tag, &mut diagnostics, settings);
|
||||
static_errors(&mut diagnostics, comment, *token_range, &tag);
|
||||
|
||||
// TD003
|
||||
let mut has_issue_link = false;
|
||||
while let Some((token, token_range)) = iter.peek() {
|
||||
if let Tok::Comment(comment) = token {
|
||||
if detect_tag(comment, token_range).is_some() {
|
||||
break;
|
||||
}
|
||||
if ISSUE_LINK_REGEX_SET.is_match(comment) {
|
||||
has_issue_link = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !has_issue_link {
|
||||
diagnostics.push(Diagnostic::new(MissingTodoLink, tag.range));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
||||
/// Returns the tag pulled out of a given comment, if it exists.
|
||||
fn detect_tag<'a>(comment: &'a str, comment_range: &'a TextRange) -> Option<Tag<'a>> {
|
||||
let Some(regex_index) = TODO_REGEX_SET.matches(comment).into_iter().next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let tag_length = PATTERN_TAG_LENGTH[regex_index];
|
||||
|
||||
let mut tag_start_offset = 0usize;
|
||||
for (i, char) in comment.chars().enumerate() {
|
||||
// Regex ensures that the first letter in the comment is the first letter of the tag.
|
||||
if char.is_alphabetic() {
|
||||
tag_start_offset = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Tag {
|
||||
content: &comment[tag_start_offset..tag_start_offset + tag_length],
|
||||
range: TextRange::at(
|
||||
comment_range.start() + TextSize::try_from(tag_start_offset).ok().unwrap(),
|
||||
TextSize::try_from(tag_length).ok().unwrap(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check that the tag itself is valid. This function modifies `diagnostics` in-place.
|
||||
fn tag_errors(tag: &Tag, diagnostics: &mut Vec<Diagnostic>, settings: &Settings) {
|
||||
if tag.content == "TODO" {
|
||||
return;
|
||||
}
|
||||
|
||||
if tag.content.to_uppercase() == "TODO" {
|
||||
// TD006
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
InvalidTodoCapitalization {
|
||||
tag: tag.content.to_string(),
|
||||
},
|
||||
tag.range,
|
||||
);
|
||||
|
||||
if settings.rules.should_fix(Rule::InvalidTodoCapitalization) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
"TODO".to_string(),
|
||||
tag.range,
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
} else {
|
||||
// TD001
|
||||
diagnostics.push(Diagnostic::new(
|
||||
InvalidTodoTag {
|
||||
tag: tag.content.to_string(),
|
||||
},
|
||||
tag.range,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for "static" errors in the comment: missing colon, missing author, etc. This function
|
||||
/// modifies `diagnostics` in-place.
|
||||
fn static_errors(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
comment: &str,
|
||||
comment_range: TextRange,
|
||||
tag: &Tag,
|
||||
) {
|
||||
let post_tag = &comment[usize::from(tag.range.end() - comment_range.start())..];
|
||||
let trimmed = post_tag.trim_start();
|
||||
let content_offset = post_tag.text_len() - trimmed.text_len();
|
||||
|
||||
let author_end = content_offset
|
||||
+ if trimmed.starts_with('(') {
|
||||
if let Some(end_index) = trimmed.find(')') {
|
||||
TextSize::try_from(end_index + 1).unwrap()
|
||||
} else {
|
||||
trimmed.text_len()
|
||||
}
|
||||
} else {
|
||||
diagnostics.push(Diagnostic::new(MissingTodoAuthor, tag.range));
|
||||
|
||||
TextSize::new(0)
|
||||
};
|
||||
|
||||
let post_author = &post_tag[usize::from(author_end)..];
|
||||
|
||||
let post_colon = if let Some((.., after_colon)) = post_author.split_once(':') {
|
||||
if let Some(stripped) = after_colon.strip_prefix(' ') {
|
||||
stripped
|
||||
} else {
|
||||
diagnostics.push(Diagnostic::new(MissingSpaceAfterTodoColon, tag.range));
|
||||
after_colon
|
||||
}
|
||||
} else {
|
||||
diagnostics.push(Diagnostic::new(MissingTodoColon, tag.range));
|
||||
""
|
||||
};
|
||||
|
||||
if post_colon.is_empty() {
|
||||
diagnostics.push(Diagnostic::new(MissingTodoDescription, tag.range));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_tag() {
|
||||
let test_comment = "# TODO: todo tag";
|
||||
let expected = Tag {
|
||||
content: "TODO",
|
||||
range: TextRange::new(TextSize::new(2), TextSize::new(6)),
|
||||
};
|
||||
assert_eq!(
|
||||
Some(expected),
|
||||
detect_tag(
|
||||
test_comment,
|
||||
&TextRange::new(TextSize::new(0), TextSize::new(15)),
|
||||
)
|
||||
);
|
||||
|
||||
let test_comment = "#TODO: todo tag";
|
||||
let expected = Tag {
|
||||
content: "TODO",
|
||||
range: TextRange::new(TextSize::new(1), TextSize::new(5)),
|
||||
};
|
||||
assert_eq!(
|
||||
Some(expected),
|
||||
detect_tag(
|
||||
test_comment,
|
||||
&TextRange::new(TextSize::new(0), TextSize::new(15)),
|
||||
)
|
||||
);
|
||||
|
||||
let test_comment = "# todo: todo tag";
|
||||
let expected = Tag {
|
||||
content: "todo",
|
||||
range: TextRange::new(TextSize::new(2), TextSize::new(6)),
|
||||
};
|
||||
assert_eq!(
|
||||
Some(expected),
|
||||
detect_tag(
|
||||
test_comment,
|
||||
&TextRange::new(TextSize::new(0), TextSize::new(15)),
|
||||
)
|
||||
);
|
||||
let test_comment = "# fixme: fixme tag";
|
||||
let expected = Tag {
|
||||
content: "fixme",
|
||||
range: TextRange::new(TextSize::new(2), TextSize::new(7)),
|
||||
};
|
||||
assert_eq!(
|
||||
Some(expected),
|
||||
detect_tag(
|
||||
test_comment,
|
||||
&TextRange::new(TextSize::new(0), TextSize::new(17)),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD006.py:4:3: TD006 [*] Invalid TODO capitalization: `ToDo` should be `TODO`
|
||||
|
|
||||
4 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
5 | # TDO006 - error
|
||||
6 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
| ^^^^ TD006
|
||||
7 | # todo (evanrittenhouse): another invalid capitalization
|
||||
|
|
||||
= help: Replace `ToDo` with `TODO`
|
||||
|
||||
ℹ Fix
|
||||
1 1 | # TDO006 - accepted
|
||||
2 2 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
3 3 | # TDO006 - error
|
||||
4 |-# ToDo (evanrittenhouse): invalid capitalization
|
||||
4 |+# TODO (evanrittenhouse): invalid capitalization
|
||||
5 5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
|
||||
TD006.py:5:3: TD006 [*] Invalid TODO capitalization: `todo` should be `TODO`
|
||||
|
|
||||
5 | # TDO006 - error
|
||||
6 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
7 | # todo (evanrittenhouse): another invalid capitalization
|
||||
| ^^^^ TD006
|
||||
|
|
||||
= help: Replace `todo` with `TODO`
|
||||
|
||||
ℹ Fix
|
||||
2 2 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
3 3 | # TDO006 - error
|
||||
4 4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
5 |-# todo (evanrittenhouse): another invalid capitalization
|
||||
5 |+# TODO (evanrittenhouse): another invalid capitalization
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD001.py:7:3: TD001 Invalid TODO tag: `XXX`
|
||||
|
|
||||
7 | # T001 - errors
|
||||
8 | # XXX (evanrittenhouse): this is not fine
|
||||
| ^^^ TD001
|
||||
9 | # FIXME (evanrittenhouse): this is not fine
|
||||
|
|
||||
|
||||
TD001.py:8:3: TD001 Invalid TODO tag: `FIXME`
|
||||
|
|
||||
8 | # T001 - errors
|
||||
9 | # XXX (evanrittenhouse): this is not fine
|
||||
10 | # FIXME (evanrittenhouse): this is not fine
|
||||
| ^^^^^ TD001
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD007.py:5:3: TD007 Missing space after colon in TODO
|
||||
|
|
||||
5 | # TODO: so does this
|
||||
6 | # T007 - errors
|
||||
7 | # TODO(evanrittenhouse):this has no space after a colon
|
||||
| ^^^^ TD007
|
||||
8 | # TODO (evanrittenhouse):this doesn't either
|
||||
9 | # TODO:neither does this
|
||||
|
|
||||
|
||||
TD007.py:6:3: TD007 Missing space after colon in TODO
|
||||
|
|
||||
6 | # T007 - errors
|
||||
7 | # TODO(evanrittenhouse):this has no space after a colon
|
||||
8 | # TODO (evanrittenhouse):this doesn't either
|
||||
| ^^^^ TD007
|
||||
9 | # TODO:neither does this
|
||||
10 | # FIXME:and lastly neither does this
|
||||
|
|
||||
|
||||
TD007.py:7:3: TD007 Missing space after colon in TODO
|
||||
|
|
||||
7 | # TODO(evanrittenhouse):this has no space after a colon
|
||||
8 | # TODO (evanrittenhouse):this doesn't either
|
||||
9 | # TODO:neither does this
|
||||
| ^^^^ TD007
|
||||
10 | # FIXME:and lastly neither does this
|
||||
|
|
||||
|
||||
TD007.py:8:3: TD007 Missing space after colon in TODO
|
||||
|
|
||||
8 | # TODO (evanrittenhouse):this doesn't either
|
||||
9 | # TODO:neither does this
|
||||
10 | # FIXME:and lastly neither does this
|
||||
| ^^^^^ TD007
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD002.py:5:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
||||
|
|
||||
5 | # TODO(evanrittenhouse): this also has an author
|
||||
6 | # T002 - errors
|
||||
7 | # TODO: this has no author
|
||||
| ^^^^ TD002
|
||||
8 | # FIXME: neither does this
|
||||
9 | # TODO : and neither does this
|
||||
|
|
||||
|
||||
TD002.py:6:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
||||
|
|
||||
6 | # T002 - errors
|
||||
7 | # TODO: this has no author
|
||||
8 | # FIXME: neither does this
|
||||
| ^^^^^ TD002
|
||||
9 | # TODO : and neither does this
|
||||
|
|
||||
|
||||
TD002.py:7:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
||||
|
|
||||
7 | # TODO: this has no author
|
||||
8 | # FIXME: neither does this
|
||||
9 | # TODO : and neither does this
|
||||
| ^^^^ TD002
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD004.py:4:3: TD004 Missing colon in TODO
|
||||
|
|
||||
4 | # TODO(evanrittenhouse): this has a colon
|
||||
5 | # T004 - errors
|
||||
6 | # TODO this has no colon
|
||||
| ^^^^ TD004
|
||||
7 | # TODO(evanrittenhouse 😀) this has no colon
|
||||
8 | # FIXME add a colon
|
||||
|
|
||||
|
||||
TD004.py:5:3: TD004 Missing colon in TODO
|
||||
|
|
||||
5 | # T004 - errors
|
||||
6 | # TODO this has no colon
|
||||
7 | # TODO(evanrittenhouse 😀) this has no colon
|
||||
| ^^^^ TD004
|
||||
8 | # FIXME add a colon
|
||||
|
|
||||
|
||||
TD004.py:6:3: TD004 Missing colon in TODO
|
||||
|
|
||||
6 | # TODO this has no colon
|
||||
7 | # TODO(evanrittenhouse 😀) this has no colon
|
||||
8 | # FIXME add a colon
|
||||
| ^^^^^ TD004
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD005.py:4:3: TD005 Missing issue description after `TODO`
|
||||
|
|
||||
4 | # TODO(evanrittenhouse): this has text, while the errors do not
|
||||
5 | # T005 - errors
|
||||
6 | # TODO(evanrittenhouse):
|
||||
| ^^^^ TD005
|
||||
7 | # TODO(evanrittenhouse)
|
||||
8 | # FIXME
|
||||
|
|
||||
|
||||
TD005.py:5:3: TD005 Missing issue description after `TODO`
|
||||
|
|
||||
5 | # T005 - errors
|
||||
6 | # TODO(evanrittenhouse):
|
||||
7 | # TODO(evanrittenhouse)
|
||||
| ^^^^ TD005
|
||||
8 | # FIXME
|
||||
|
|
||||
|
||||
TD005.py:6:3: TD005 Missing issue description after `TODO`
|
||||
|
|
||||
6 | # TODO(evanrittenhouse):
|
||||
7 | # TODO(evanrittenhouse)
|
||||
8 | # FIXME
|
||||
| ^^^^^ TD005
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD003.py:9:3: TD003 Missing issue link on the line following this TODO
|
||||
|
|
||||
9 | # TDO003 - errors
|
||||
10 | # TODO: this comment has no
|
||||
| ^^^^ TD003
|
||||
11 | # link after it
|
||||
|
|
||||
|
||||
TD003.py:12:3: TD003 Missing issue link on the line following this TODO
|
||||
|
|
||||
12 | # link after it
|
||||
13 |
|
||||
14 | # TODO: here's a TODO with no link after it
|
||||
| ^^^^ TD003
|
||||
15 | def foo(x):
|
||||
16 | return x
|
||||
|
|
||||
|
||||
TD003.py:25:3: TD003 Missing issue link on the line following this TODO
|
||||
|
|
||||
25 | # TDO-3870
|
||||
26 |
|
||||
27 | # TODO: here's a TODO without an issue link
|
||||
| ^^^^ TD003
|
||||
28 | # TODO: followed by a new TODO with an issue link
|
||||
29 | # TDO-3870
|
||||
|
|
||||
|
||||
TD003.py:29:3: TD003 Missing issue link on the line following this TODO
|
||||
|
|
||||
29 | # TDO-3870
|
||||
30 |
|
||||
31 | # TODO: here's a TODO on the last line with no link
|
||||
| ^^^^ TD003
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -29,6 +29,7 @@ pub mod flake8_return;
|
|||
pub mod flake8_self;
|
||||
pub mod flake8_simplify;
|
||||
pub mod flake8_tidy_imports;
|
||||
pub mod flake8_todos;
|
||||
pub mod flake8_type_checking;
|
||||
pub mod flake8_unused_arguments;
|
||||
pub mod flake8_use_pathlib;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ natively, including:
|
|||
- [flake8-simplify](https://pypi.org/project/flake8-simplify/)
|
||||
- [flake8-super](https://pypi.org/project/flake8-super/)
|
||||
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
|
||||
- [flake8-todos](https://pypi.org/project/flake8-todos/)
|
||||
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
|
||||
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/charliermarsh/ruff/issues/2102))
|
||||
|
|
@ -161,6 +162,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
|
|||
- [flake8-simplify](https://pypi.org/project/flake8-simplify/)
|
||||
- [flake8-super](https://pypi.org/project/flake8-super/)
|
||||
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
|
||||
- [flake8-todos](https://pypi.org/project/flake8-todos/)
|
||||
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
|
||||
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/charliermarsh/ruff/issues/2102))
|
||||
|
|
|
|||
|
|
@ -2296,6 +2296,16 @@
|
|||
"TCH003",
|
||||
"TCH004",
|
||||
"TCH005",
|
||||
"TD",
|
||||
"TD0",
|
||||
"TD00",
|
||||
"TD001",
|
||||
"TD002",
|
||||
"TD003",
|
||||
"TD004",
|
||||
"TD005",
|
||||
"TD006",
|
||||
"TD007",
|
||||
"TID",
|
||||
"TID2",
|
||||
"TID25",
|
||||
|
|
|
|||
Loading…
Reference in New Issue