From ab3c02342bf9d87a6ee9cdbadd3d5626a3fea2ec Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Sat, 10 Jun 2023 19:17:58 -0700 Subject: [PATCH] Implement copyright notice detection (#4701) ## Summary Add copyright notice detection to enforce the presence of copyright headers in Python files. Configurable settings include: the relevant regular expression, the author name, and the minimum file size, similar to [flake8-copyright](https://github.com/savoirfairelinux/flake8-copyright). Closes https://github.com/charliermarsh/ruff/issues/3579 --------- Signed-off-by: ryan Co-authored-by: Charlie Marsh --- README.md | 1 + crates/ruff/src/checkers/physical_lines.rs | 8 + crates/ruff/src/codes.rs | 3 + crates/ruff/src/registry.rs | 4 + crates/ruff/src/rules/copyright/mod.rs | 158 ++++++++++++++++++ .../rules/missing_copyright_notice.rs | 59 +++++++ crates/ruff/src/rules/copyright/rules/mod.rs | 3 + crates/ruff/src/rules/copyright/settings.rs | 94 +++++++++++ ...les__copyright__tests__invalid_author.snap | 12 ++ ..._rules__copyright__tests__late_notice.snap | 12 ++ ...ruff__rules__copyright__tests__notice.snap | 4 + ...ules__copyright__tests__notice_with_c.snap | 4 + ...s__copyright__tests__notice_with_caps.snap | 4 + ...__copyright__tests__notice_with_range.snap | 4 + ...__rules__copyright__tests__small_file.snap | 4 + ...rules__copyright__tests__valid_author.snap | 4 + crates/ruff/src/rules/mod.rs | 1 + crates/ruff/src/settings/configuration.rs | 12 +- crates/ruff/src/settings/defaults.rs | 10 +- crates/ruff/src/settings/mod.rs | 11 +- crates/ruff/src/settings/options.rs | 12 +- crates/ruff_macros/src/rule_namespace.rs | 8 +- crates/ruff_wasm/src/lib.rs | 10 +- docs/faq.md | 2 + ruff.schema.json | 44 +++++ scripts/pyproject.toml | 1 + 26 files changed, 465 insertions(+), 24 deletions(-) create mode 100644 crates/ruff/src/rules/copyright/mod.rs create mode 100644 crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs create mode 100644 crates/ruff/src/rules/copyright/rules/mod.rs create mode 100644 crates/ruff/src/rules/copyright/settings.rs create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap create mode 100644 crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap diff --git a/README.md b/README.md index b23d42d610..86755f5c23 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ quality tools, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 3a5345ae58..8934a75762 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -8,6 +8,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; use crate::registry::Rule; +use crate::rules::copyright::rules::missing_copyright_notice; use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, @@ -49,6 +50,7 @@ pub(crate) fn check_physical_lines( let enforce_blank_line_contains_whitespace = settings.rules.enabled(Rule::BlankLineWithWhitespace); let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation); + let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice); let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration); let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace); @@ -172,6 +174,12 @@ pub(crate) fn check_physical_lines( } } + if enforce_copyright_notice { + if let Some(diagnostic) = missing_copyright_notice(locator, settings) { + diagnostics.push(diagnostic); + } + } + diagnostics } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 9e4f05bd97..62dfc0ed77 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -374,6 +374,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "401") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet), (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), + // copyright + (Copyright, "001") => (RuleGroup::Unspecified, rules::copyright::rules::MissingCopyrightNotice), + // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), (Pyupgrade, "003") => (RuleGroup::Unspecified, rules::pyupgrade::rules::TypeOfPrimitive), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 5100ec9384..c89df825dd 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -80,6 +80,9 @@ pub enum Linter { /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, + /// Copyright-related rules + #[prefix = "CPY"] + Copyright, /// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) #[prefix = "C4"] Flake8Comprehensions, @@ -267,6 +270,7 @@ impl Rule { | Rule::ShebangLeadingWhitespace | Rule::TrailingWhitespace | Rule::TabIndentation + | Rule::MissingCopyrightNotice | Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring diff --git a/crates/ruff/src/rules/copyright/mod.rs b/crates/ruff/src/rules/copyright/mod.rs new file mode 100644 index 0000000000..ceadac5efb --- /dev/null +++ b/crates/ruff/src/rules/copyright/mod.rs @@ -0,0 +1,158 @@ +//! Rules related to copyright notices. +pub(crate) mod rules; + +pub mod settings; + +#[cfg(test)] +mod tests { + use crate::registry::Rule; + use crate::test::test_snippet; + use crate::{assert_messages, settings}; + + #[test] + fn notice() { + let diagnostics = test_snippet( + r#" +# Copyright 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_c() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_caps() { + let diagnostics = test_snippet( + r#" +# COPYRIGHT (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_range() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2021-2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn valid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Ruff + +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn invalid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Some Author + +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn small_file() { + let diagnostics = test_snippet( + r#" +import os +"# + .trim(), + &settings::Settings { + copyright: super::settings::Settings { + min_file_size: 256, + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn late_notice() { + let diagnostics = test_snippet( + r#" +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content + +# Copyright 2023 +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } +} diff --git a/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs b/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs new file mode 100644 index 0000000000..00ceccffee --- /dev/null +++ b/crates/ruff/src/rules/copyright/rules/missing_copyright_notice.rs @@ -0,0 +1,59 @@ +use ruff_text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; + +use crate::settings::Settings; + +/// ## What it does +/// Checks for the absence of copyright notices within Python files. +/// +/// ## Why is this bad? +/// In some codebases, it's common to have a license header at the top of every +/// file. This rule ensures that the license header is present. +#[violation] +pub struct MissingCopyrightNotice; + +impl Violation for MissingCopyrightNotice { + #[derive_message_formats] + fn message(&self) -> String { + format!("Missing copyright notice at top of file") + } +} + +/// CPY001 +pub(crate) fn missing_copyright_notice( + locator: &Locator, + settings: &Settings, +) -> Option { + // Ignore files that are too small to contain a copyright notice. + if locator.len() < settings.copyright.min_file_size { + return None; + } + + // Only search the first 1024 bytes in the file. + let contents = if locator.len() < 1024 { + locator.contents() + } else { + locator.up_to(TextSize::from(1024)) + }; + + // Locate the copyright notice. + if let Some(match_) = settings.copyright.notice_rgx.find(contents) { + match settings.copyright.author { + Some(ref author) => { + // Ensure that it's immediately followed by the author. + if contents[match_.end()..].trim_start().starts_with(author) { + return None; + } + } + None => return None, + } + } + + Some(Diagnostic::new( + MissingCopyrightNotice, + TextRange::default(), + )) +} diff --git a/crates/ruff/src/rules/copyright/rules/mod.rs b/crates/ruff/src/rules/copyright/rules/mod.rs new file mode 100644 index 0000000000..860448155f --- /dev/null +++ b/crates/ruff/src/rules/copyright/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use missing_copyright_notice::{missing_copyright_notice, MissingCopyrightNotice}; + +mod missing_copyright_notice; diff --git a/crates/ruff/src/rules/copyright/settings.rs b/crates/ruff/src/rules/copyright/settings.rs new file mode 100644 index 0000000000..d3849a8752 --- /dev/null +++ b/crates/ruff/src/rules/copyright/settings.rs @@ -0,0 +1,94 @@ +//! Settings for the `copyright` plugin. + +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "CopyrightOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"(?i)Copyright\s+(\(C\)\s+)?\d{4}([-,]\d{4})*"#, + value_type = "str", + example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""# + )] + /// The regular expression used to match the copyright notice, compiled + /// with the [`regex`](https://docs.rs/regex/latest/regex/) crate. + /// + /// Defaults to `(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*`, which matches + /// the following: + /// - `Copyright 2023` + /// - `Copyright (C) 2023` + /// - `Copyright 2021-2023` + /// - `Copyright (C) 2021-2023` + pub notice_rgx: Option, + #[option(default = "None", value_type = "str", example = r#"author = "Ruff""#)] + /// Author to enforce within the copyright notice. If provided, the + /// author must be present immediately following the copyright notice. + pub author: Option, + #[option( + default = r#"0"#, + value_type = "int", + example = r#" + # Avoid enforcing a header on files smaller than 1024 bytes. + min-file-size = 1024 + "# + )] + /// A minimum file size (in bytes) required for a copyright notice to + /// be enforced. By default, all files are validated. + pub min_file_size: Option, +} + +#[derive(Debug, CacheKey)] +pub struct Settings { + pub notice_rgx: Regex, + pub author: Option, + pub min_file_size: usize, +} + +static COPYRIGHT: Lazy = + Lazy::new(|| Regex::new(r"(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*").unwrap()); + +impl Default for Settings { + fn default() -> Self { + Self { + notice_rgx: COPYRIGHT.clone(), + author: None, + min_file_size: 0, + } + } +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + notice_rgx: options + .notice_rgx + .map(|pattern| Regex::new(&pattern)) + .transpose() + .expect("Invalid `notice-rgx`") + .unwrap_or_else(|| COPYRIGHT.clone()), + author: options.author, + min_file_size: options.min_file_size.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + notice_rgx: Some(settings.notice_rgx.to_string()), + author: settings.author, + min_file_size: Some(settings.min_file_size), + } + } +} diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap new file mode 100644 index 0000000000..2b09c76351 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__invalid_author.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Copyright (C) 2023 Some Author + | CPY001 +2 | +3 | import os + | + + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap new file mode 100644 index 0000000000..0bc581e98d --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__late_notice.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Content Content Content Content Content Content Content Content Content Content + | CPY001 +2 | # Content Content Content Content Content Content Content Content Content Content +3 | # Content Content Content Content Content Content Content Content Content Content + | + + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_c.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_caps.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__notice_with_range.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__small_file.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap new file mode 100644 index 0000000000..2a35fab682 --- /dev/null +++ b/crates/ruff/src/rules/copyright/snapshots/ruff__rules__copyright__tests__valid_author.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 23083cb2ab..cc06dfb91d 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::useless_format)] pub mod airflow; +pub mod copyright; pub mod eradicate; pub mod flake8_2020; pub mod flake8_annotations; diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 16acabe30a..213128b966 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -16,10 +16,11 @@ use crate::fs; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -74,6 +75,7 @@ pub struct Configuration { pub flake8_bugbear: Option, pub flake8_builtins: Option, pub flake8_comprehensions: Option, + pub copyright: Option, pub flake8_errmsg: Option, pub flake8_implicit_str_concat: Option, pub flake8_import_conventions: Option, @@ -227,6 +229,7 @@ impl Configuration { flake8_bugbear: options.flake8_bugbear, flake8_builtins: options.flake8_builtins, flake8_comprehensions: options.flake8_comprehensions, + copyright: options.copyright, flake8_errmsg: options.flake8_errmsg, flake8_gettext: options.flake8_gettext, flake8_implicit_str_concat: options.flake8_implicit_str_concat, @@ -305,6 +308,7 @@ impl Configuration { flake8_comprehensions: self .flake8_comprehensions .combine(config.flake8_comprehensions), + copyright: self.copyright.combine(config.copyright), flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg), flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext), flake8_implicit_str_concat: self diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index aa5007789d..129c3a16f2 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -10,10 +10,11 @@ use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; use crate::rule_selector::{prefix_to_selector, RuleSelector}; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::types::FilePatternSet; @@ -95,6 +96,7 @@ impl Default for Settings { flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_builtins: flake8_builtins::settings::Settings::default(), flake8_comprehensions: flake8_comprehensions::settings::Settings::default(), + copyright: copyright::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings::default(), flake8_import_conventions: flake8_import_conventions::settings::Settings::default(), diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index f2e2aec2db..c3878d7eda 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -16,10 +16,11 @@ use ruff_macros::CacheKey; use crate::registry::{Rule, RuleNamespace, RuleSet, INCOMPATIBLE_CODES}; use crate::rule_selector::{RuleSelector, Specificity}; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -111,6 +112,7 @@ pub struct Settings { pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_builtins: flake8_builtins::settings::Settings, pub flake8_comprehensions: flake8_comprehensions::settings::Settings, + pub copyright: copyright::settings::Settings, pub flake8_errmsg: flake8_errmsg::settings::Settings, pub flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings, pub flake8_import_conventions: flake8_import_conventions::settings::Settings, @@ -199,6 +201,7 @@ impl Settings { .flake8_comprehensions .map(Into::into) .unwrap_or_default(), + copyright: config.copyright.map(Into::into).unwrap_or_default(), flake8_errmsg: config.flake8_errmsg.map(Into::into).unwrap_or_default(), flake8_implicit_str_concat: config .flake8_implicit_str_concat diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 0a7462cc8f..7c99dd8a2c 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -8,10 +8,11 @@ use ruff_macros::ConfigurationOptions; use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -495,6 +496,9 @@ pub struct Options { /// Options for the `flake8-comprehensions` plugin. pub flake8_comprehensions: Option, #[option_group] + /// Options for the `copyright` plugin. + pub copyright: Option, + #[option_group] /// Options for the `flake8-errmsg` plugin. pub flake8_errmsg: Option, #[option_group] diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 67dd7c4070..f1bd7f5adf 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -15,9 +15,9 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", ); - let mut url_match_arms = quote!(Self::Ruff => None, Self::Numpy => None, ); + let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", Self::Copyright => "Copyright-related rules", ); + let mut url_match_arms = + quote!(Self::Ruff => None, Self::Numpy => None, Self::Copyright => None, ); let mut all_prefixes = HashSet::new(); @@ -59,7 +59,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result #name,}); url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),}); diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 84566771dd..3b4a66db5e 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -9,10 +9,11 @@ use ruff::line_width::{LineLength, TabSize}; use ruff::linter::{check_path, LinterResult}; use ruff::registry::AsRule; use ruff::rules::{ - flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + copyright, flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, + flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; @@ -137,6 +138,7 @@ pub fn defaultSettings() -> Result { flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()), flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()), flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()), + copyright: Some(copyright::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()), diff --git a/docs/faq.md b/docs/faq.md index 3ee9baca50..07167d2829 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -41,6 +41,7 @@ natively, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) @@ -143,6 +144,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-comprehensions/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) diff --git a/ruff.schema.json b/ruff.schema.json index 69702b9f17..e23fad53f2 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -32,6 +32,17 @@ "null" ] }, + "copyright": { + "description": "Options for the `copyright` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/CopyrightOptions" + }, + { + "type": "null" + } + ] + }, "dummy-variable-rgx": { "description": "A regular expression used to identify \"dummy\" variables, or those which should be ignored when enforcing (e.g.) unused-variable rules. The default expression matches `_`, `__`, and `_var`, but not `_var_`.", "type": [ @@ -620,6 +631,35 @@ } ] }, + "CopyrightOptions": { + "type": "object", + "properties": { + "author": { + "description": "Author to enforce within the copyright notice. If provided, the author must be present immediately following the copyright notice.", + "type": [ + "string", + "null" + ] + }, + "min-file-size": { + "description": "A minimum file size (in bytes) required for a copyright notice to be enforced. By default, all files are validated.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "notice-rgx": { + "description": "The regular expression used to match the copyright notice, compiled with the [`regex`](https://docs.rs/regex/latest/regex/) crate.\n\nDefaults to `(?i)Copyright\\s+(\\(C\\)\\s+)?\\d{4}(-\\d{4})*`, which matches the following: - `Copyright 2023` - `Copyright (C) 2023` - `Copyright 2021-2023` - `Copyright (C) 2021-2023`", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "Flake8AnnotationsOptions": { "type": "object", "properties": { @@ -1679,6 +1719,10 @@ "COM812", "COM818", "COM819", + "CPY", + "CPY0", + "CPY00", + "CPY001", "D", "D1", "D10", diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index c34ba92a24..ca7d45e508 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -15,6 +15,7 @@ ignore = [ "D", # pydocstyle "PL", # pylint "S", # bandit + "CPY", # copyright "G", # flake8-logging "T", # flake8-print "FBT", # flake8-boolean-trap