From 9a6b84e080331dd1e0e330e9eb14641868075fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sat, 9 Nov 2024 20:19:23 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.toml | 170 ++++++++++++++++++ Makefile | 25 +++ src/main.rs | 505 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 702 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..697727f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,170 @@ +[package] +name = "clippy-lints" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "1.*", default-features = false } +clap = { version = "4.*", default-features = false, features = [ + "derive", + "help", + "std", + "suggestions", + "usage", +] } +serde = { version = "1.*", default-features = false, features = ["derive"] } +ureq = { version = "2.*", default-features = false, features = ["json", "tls"] } + +[lints.clippy] +# enabled groups +correctness = { level = "deny", priority = -1 } +suspicious = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } + +# pedantic overrides +too_many_lines = "allow" +must_use_candidate = "allow" +map_unwrap_or = "allow" +missing_errors_doc = "allow" +if_not_else = "allow" + +# nursery overrides +missing_const_for_fn = "allow" +option_if_let_else = "allow" + +# complexity overrides +too_many_arguments = "allow" + +# style overrides +new_without_default = "allow" + +# cargo overrides +multiple_crate_versions = "allow" +cargo_common_metadata = "allow" + +# selected restrictions +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +arithmetic_side_effects = "warn" +as_conversions = "warn" +assertions_on_result_states = "warn" +cfg_not_test = "warn" +clone_on_ref_ptr = "warn" +create_dir = "warn" +dbg_macro = "warn" +decimal_literal_representation = "warn" +default_numeric_fallback = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +else_if_without_else = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +exit = "warn" +filetype_is_file = "warn" +float_arithmetic = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +format_push_string = "warn" +get_unwrap = "warn" +indexing_slicing = "warn" +infinite_loop = "warn" +inline_asm_x86_att_syntax = "warn" +inline_asm_x86_intel_syntax = "warn" +integer_division = "warn" +iter_over_hash_type = "warn" +large_include_file = "warn" +let_underscore_must_use = "warn" +let_underscore_untyped = "warn" +little_endian_bytes = "warn" +lossy_float_literal = "warn" +map_err_ignore = "warn" +mem_forget = "warn" +missing_assert_message = "warn" +missing_asserts_for_indexing = "warn" +mixed_read_write_in_expression = "warn" +modulo_arithmetic = "warn" +multiple_inherent_impl = "warn" +multiple_unsafe_ops_per_block = "warn" +mutex_atomic = "warn" +panic = "warn" +partial_pub_fields = "warn" +pattern_type_mismatch = "warn" +print_stderr = "warn" +print_stdout = "warn" +pub_without_shorthand = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +rest_pat_in_fully_bound_structs = "warn" +same_name_method = "warn" +self_named_module_files = "warn" +semicolon_inside_block = "warn" +str_to_string = "warn" +string_add = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" +string_to_string = "warn" +suspicious_xor_used_as_pow = "warn" +tests_outside_test_module = "warn" +todo = "warn" +try_err = "warn" +undocumented_unsafe_blocks = "warn" +unimplemented = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unnecessary_self_imports = "warn" +unneeded_field_pattern = "warn" +unseparated_literal_suffix = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +use_debug = "warn" +verbose_file_reads = "warn" + +# restrictions explicit allows +absolute_paths = "allow" +alloc_instead_of_core = "allow" +as_underscore = "allow" +big_endian_bytes = "allow" +default_union_representation = "allow" +error_impl_error = "allow" +exhaustive_enums = "allow" +exhaustive_structs = "allow" +expect_used = "allow" +field_scoped_visibility_modifiers = "allow" +host_endian_bytes = "allow" +if_then_some_else_none = "allow" +impl_trait_in_params = "allow" +implicit_return = "allow" +integer_division_remainder_used = "allow" +min_ident_chars = "allow" +missing_docs_in_private_items = "allow" +missing_inline_in_public_items = "allow" +missing_trait_methods = "allow" +mod_module_files = "allow" +needless_raw_strings = "allow" +non_ascii_literal = "allow" +panic_in_result_fn = "allow" +pathbuf_init_then_push = "allow" +pub_use = "allow" +pub_with_shorthand = "allow" +question_mark_used = "allow" +ref_patterns = "allow" +semicolon_outside_block = "allow" +separated_literal_suffix = "allow" +shadow_reuse = "allow" +shadow_same = "allow" +shadow_unrelated = "allow" +single_call_fn = "allow" +single_char_lifetime_names = "allow" +std_instead_of_alloc = "allow" +std_instead_of_core = "allow" +unreachable = "allow" +unwrap_in_result = "allow" +wildcard_enum_match_arm = "allow" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6d0f9c --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: check +check: | fmt lint test + +.PHONY: docs +docs: + cargo watch -- cargo doc + +.PHONY: test +test: + cargo hack --feature-powerset --no-dev-deps check + cargo test --workspace --color=always + +.PHONY: lint +lint: + cargo clippy --workspace --tests --color=always + +.PHONY: fmt +fmt: + cargo fmt + find -name '*.md' | xargs --no-run-if-empty prettier --print-width 80 --prose-wrap always --write + find -name '*.toml' | xargs --no-run-if-empty taplo format + +.PHONY: build-static +build-static: + cargo build --target x86_64-unknown-linux-musl --no-default-features --release --workspace diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ceecafc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,505 @@ +use std::fmt::{self, Write as _}; + +use anyhow::{anyhow, Result}; +use clap::{Parser, ValueEnum}; +use serde::Deserialize; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum LintGroup { + Cargo, + Complexity, + Correctness, + Nursery, + Pedantic, + Perf, + Restriction, + Style, + Suspicious, + Deprecated, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum Profile { + Publish, + Personal, +} + +#[derive(Parser, Debug)] +struct Args { + #[arg(long)] + profile: Profile, + + #[arg(long)] + workspace: bool, +} + +impl LintGroup { + fn as_str(self) -> &'static str { + match self { + Self::Cargo => "cargo", + Self::Complexity => "complexity", + Self::Correctness => "correctness", + Self::Nursery => "nursery", + Self::Pedantic => "pedantic", + Self::Perf => "perf", + Self::Restriction => "restriction", + Self::Style => "style", + Self::Suspicious => "suspicious", + Self::Deprecated => "deprecated", + } + } +} + +impl fmt::Display for LintGroup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +enum LintLevel { + Allow, + Warn, + Deny, + None, +} + +impl LintLevel { + fn as_str(self) -> &'static str { + match self { + Self::Allow => "allow", + Self::Warn => "warn", + Self::Deny => "deny", + Self::None => "none", + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[expect(dead_code, reason = "this is an external data definition")] +struct Lint { + id: LintId, + group: LintGroup, + #[serde(rename = "level")] + default_level: LintLevel, + version: String, +} + +#[derive(Clone, Copy, Debug)] +enum PrioritySetting { + Explicit(isize), + Unspecified, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +struct LintId(String); + +impl fmt::Display for LintId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +struct LintList(Vec); + +impl From> for LintList { + fn from(value: Vec<&str>) -> Self { + Self(value.into_iter().map(|s| LintId(s.to_owned())).collect()) + } +} + +#[derive(Debug)] +struct SingleLintConfig { + lint: LintId, + priority: PrioritySetting, + level: LintLevel, +} + +#[derive(Debug)] +struct GroupConfig { + group: LintGroup, + priority: PrioritySetting, + level: LintLevel, +} + +#[derive(Debug)] +enum Setting { + Single(SingleLintConfig), + Group(GroupConfig), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ExhaustiveGroupClassification { + Default, + Exception, +} + +#[derive(Debug)] +struct ExhausiveGroup { + defaults: Vec, + exceptions: Vec, +} + +struct Exceptions { + level: LintLevel, + lints: LintList, +} + +impl Setting { + fn set_group(group: LintGroup, priority: PrioritySetting, level: LintLevel) -> Self { + Self::Group(GroupConfig { + group, + priority, + level, + }) + } + fn warn_group(group: LintGroup, priority: PrioritySetting) -> Self { + Self::set_group(group, priority, LintLevel::Warn) + } + + fn deny_group(group: LintGroup, priority: PrioritySetting) -> Self { + Self::set_group(group, priority, LintLevel::Deny) + } + + fn allow(response: &Response, group: LintGroup, lints: &[&str]) -> Result> { + lints + .iter() + .map(|lint| { + let lint = LintId((*lint).to_owned()); + let found = response.0.iter().find(|r| r.id == lint && r.group == group); + if found.is_none() { + Err(anyhow!("lint {} not in group {}", lint, group.as_str())) + } else { + Ok(Self::Single(SingleLintConfig { + lint, + priority: PrioritySetting::Unspecified, + level: LintLevel::Allow, + })) + } + }) + .collect() + } + + fn split_group_exhaustive( + response: &Response, + group: LintGroup, + default_level: LintLevel, + exceptions: &Exceptions, + ) -> Result { + let all_lints_in_group: Vec = response + .0 + .iter() + .filter_map(|lint| { + if lint.group == group { + Some(lint.id.clone()) + } else { + None + } + }) + .collect(); + + if let Some(err) = exceptions.lints.0.iter().find_map(|lint| { + if !all_lints_in_group.contains(lint) { + Some(anyhow!("lint {lint} not part of group {group}")) + } else { + None + } + }) { + return Err(err); + }; + + Ok(all_lints_in_group + .into_iter() + .map(|lint| { + if exceptions.lints.0.contains(&lint) { + ( + ExhaustiveGroupClassification::Exception, + Self::Single(SingleLintConfig { + lint, + priority: PrioritySetting::Unspecified, + level: exceptions.level, + }), + ) + } else { + ( + ExhaustiveGroupClassification::Default, + Self::Single(SingleLintConfig { + lint, + priority: PrioritySetting::Unspecified, + level: default_level, + }), + ) + } + }) + .fold( + ExhausiveGroup { + defaults: Vec::new(), + exceptions: Vec::new(), + }, + |mut acc, (classification, setting)| { + match classification { + ExhaustiveGroupClassification::Default => acc.defaults.push(setting), + ExhaustiveGroupClassification::Exception => acc.exceptions.push(setting), + }; + acc + }, + )) + } +} + +#[derive(Debug)] +struct ConfigGroup { + comment: Option, + settings: Vec, +} + +#[derive(Debug)] +struct Config(Vec); + +impl Config { + fn to_toml(&self, args: &Args) -> String { + let mut output = if args.workspace { + String::from("[workspace.lints.clippy]\n") + } else { + String::from("[lints.clippy]\n") + }; + + let mut iter_group = self.0.iter().peekable(); + + while let Some(group) = iter_group.next() { + let last_group = iter_group.peek().is_none(); + if let Some(ref comment) = group.comment { + writeln!(output, "# {comment}").expect("writing to string succeeds"); + } + + let mut iter_setting = group.settings.iter().peekable(); + while let Some(setting) = iter_setting.next() { + let last_setting = iter_setting.peek().is_none(); + match *setting { + Setting::Single(ref single_lint_config) => match single_lint_config.priority { + PrioritySetting::Explicit(priority) => write!( + output, + "{} = {{ level = \"{}\", priority = {} }}", + single_lint_config.lint.0, + single_lint_config.level.as_str(), + priority + ) + .expect("writing to string succeeds"), + PrioritySetting::Unspecified => write!( + output, + "{} = \"{}\"", + single_lint_config.lint.0, + single_lint_config.level.as_str() + ) + .expect("writing to string succeeds"), + }, + Setting::Group(ref group_config) => match group_config.priority { + PrioritySetting::Explicit(priority) => write!( + output, + "{} = {{ level = \"{}\", priority = {} }}", + group_config.group.as_str(), + group_config.level.as_str(), + priority + ) + .expect("writing to string succeeds"), + PrioritySetting::Unspecified => write!( + output, + "{} = \"{}\"", + group_config.group.as_str(), + group_config.level.as_str(), + ) + .expect("writing to string succeeds"), + }, + }; + if !last_setting { + output.push('\n'); + } + if last_setting && !last_group { + output.push('\n'); + } + } + if !last_group { + output.push('\n'); + } + } + + output + } +} + +#[derive(Debug, Deserialize)] +struct Response(Vec); + +fn main() -> Result<()> { + let args = Args::parse(); + + let response: Response = ureq::get("https://rust-lang.github.io/rust-clippy/stable/lints.json") + .call()? + .into_json()?; + + let restriction_group = Setting::split_group_exhaustive( + &response, + LintGroup::Restriction, + LintLevel::Allow, + &Exceptions { + level: LintLevel::Warn, + lints: vec![ + "allow_attributes", + "allow_attributes_without_reason", + "arithmetic_side_effects", + "as_conversions", + "assertions_on_result_states", + "cfg_not_test", + "clone_on_ref_ptr", + "create_dir", + "dbg_macro", + "decimal_literal_representation", + "default_numeric_fallback", + "deref_by_slicing", + "disallowed_script_idents", + "else_if_without_else", + "empty_drop", + "empty_enum_variants_with_brackets", + "empty_structs_with_brackets", + "exit", + "filetype_is_file", + "float_arithmetic", + "float_cmp_const", + "fn_to_numeric_cast_any", + "format_push_string", + "get_unwrap", + "indexing_slicing", + "infinite_loop", + "inline_asm_x86_att_syntax", + "inline_asm_x86_intel_syntax", + "integer_division", + "iter_over_hash_type", + "large_include_file", + "let_underscore_must_use", + "let_underscore_untyped", + "little_endian_bytes", + "lossy_float_literal", + "map_err_ignore", + "mem_forget", + "missing_assert_message", + "missing_asserts_for_indexing", + "mixed_read_write_in_expression", + "modulo_arithmetic", + "multiple_inherent_impl", + "multiple_unsafe_ops_per_block", + "mutex_atomic", + "panic", + "partial_pub_fields", + "pattern_type_mismatch", + "print_stderr", + "print_stdout", + "pub_without_shorthand", + "rc_buffer", + "rc_mutex", + "redundant_type_annotations", + "renamed_function_params", + "rest_pat_in_fully_bound_structs", + "same_name_method", + "self_named_module_files", + "semicolon_inside_block", + "str_to_string", + "string_add", + "string_lit_chars_any", + "string_slice", + "string_to_string", + "suspicious_xor_used_as_pow", + "tests_outside_test_module", + "todo", + "try_err", + "undocumented_unsafe_blocks", + "unimplemented", + "unnecessary_safety_comment", + "unnecessary_safety_doc", + "unnecessary_self_imports", + "unneeded_field_pattern", + "unseparated_literal_suffix", + "unused_result_ok", + "unwrap_used", + "use_debug", + "verbose_file_reads", + ] + .into(), + }, + )?; + + let config = Config(vec![ + ConfigGroup { + comment: Some("enabled groups".to_owned()), + settings: vec![ + Setting::deny_group(LintGroup::Correctness, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Suspicious, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Style, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Complexity, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Perf, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Cargo, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Pedantic, PrioritySetting::Explicit(-1)), + Setting::warn_group(LintGroup::Nursery, PrioritySetting::Explicit(-1)), + ], + }, + ConfigGroup { + comment: Some("pedantic overrides".to_owned()), + settings: Setting::allow( + &response, + LintGroup::Pedantic, + &[ + "too_many_lines", + "must_use_candidate", + "map_unwrap_or", + "missing_errors_doc", + "if_not_else", + ], + )?, + }, + ConfigGroup { + comment: Some("nursery overrides".to_owned()), + settings: Setting::allow( + &response, + LintGroup::Nursery, + &["missing_const_for_fn", "option_if_let_else"], + )?, + }, + ConfigGroup { + comment: Some("complexity overrides".to_owned()), + settings: Setting::allow(&response, LintGroup::Complexity, &["too_many_arguments"])?, + }, + ConfigGroup { + comment: Some("style overrides".to_owned()), + settings: Setting::allow(&response, LintGroup::Style, &["new_without_default"])?, + }, + ConfigGroup { + comment: Some("cargo overrides".to_owned()), + settings: Setting::allow(&response, LintGroup::Cargo, &{ + let mut v = vec!["multiple_crate_versions"]; + match args.profile { + Profile::Publish => (), + Profile::Personal => v.push("cargo_common_metadata"), + } + v + })?, + }, + ConfigGroup { + comment: Some("selected restrictions".to_owned()), + settings: restriction_group.exceptions, + }, + ConfigGroup { + comment: Some("restrictions explicit allows".to_owned()), + settings: restriction_group.defaults, + }, + ]); + + let output = config.to_toml(&args); + + #[expect(clippy::print_stdout, reason = "this is the main program output")] + { + println!("{output}"); + } + + Ok(()) +}