From 240ca54659fef25aca88192817d5ac9f1b53788a Mon Sep 17 00:00:00 2001 From: harehare Date: Sun, 28 Jun 2026 15:35:15 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(mq-lint):=20add=20DuplicateImp?= =?UTF-8?q?ort=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/mq-hir/src/source.rs | 2 +- crates/mq-lint/src/message.rs | 13 ++- crates/mq-lint/src/rules/correctness.rs | 2 + .../src/rules/correctness/duplicate_import.rs | 81 +++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 crates/mq-lint/src/rules/correctness/duplicate_import.rs diff --git a/crates/mq-hir/src/source.rs b/crates/mq-hir/src/source.rs index c98ea2110..e2cddb29d 100644 --- a/crates/mq-hir/src/source.rs +++ b/crates/mq-hir/src/source.rs @@ -13,7 +13,7 @@ impl Source { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Hash)] pub struct SourceInfo { pub source_id: Option, pub text_range: Option, diff --git a/crates/mq-lint/src/message.rs b/crates/mq-lint/src/message.rs index 76e2a916e..c3238f596 100644 --- a/crates/mq-lint/src/message.rs +++ b/crates/mq-lint/src/message.rs @@ -19,6 +19,7 @@ pub enum RuleId { InfiniteLoop, DeprecatedFunctionCall, DuplicateMatchArm, + DuplicateImport, ShadowVariable, MissingElseInExpr, AlwaysTrueCondition, @@ -44,7 +45,7 @@ pub enum RuleId { impl RuleId { /// All known rule IDs. - pub const ALL: &'static [RuleId; 28] = &[ + pub const ALL: &'static [RuleId; 29] = &[ RuleId::UnusedVariable, RuleId::UnusedFunction, RuleId::UnusedImport, @@ -52,6 +53,7 @@ impl RuleId { RuleId::InfiniteLoop, RuleId::DeprecatedFunctionCall, RuleId::DuplicateMatchArm, + RuleId::DuplicateImport, RuleId::ShadowVariable, RuleId::MissingElseInExpr, RuleId::AlwaysTrueCondition, @@ -84,6 +86,7 @@ impl RuleId { RuleId::UnreachableCode => "unreachable_code", RuleId::InfiniteLoop => "infinite_loop", RuleId::DuplicateMatchArm => "duplicate_match_arm", + RuleId::DuplicateImport => "duplicate_import", RuleId::DeprecatedFunctionCall => "deprecated_function_call", RuleId::ShadowVariable => "shadow_variable", RuleId::MissingElseInExpr => "missing_else_in_expr", @@ -148,6 +151,9 @@ pub enum LintMessage { DuplicateMatchArm { pattern: String, }, + DuplicateImport { + name: String, + }, DeprecatedFunctionCall { name: String, }, @@ -227,6 +233,7 @@ impl LintMessage { LintMessage::UnreachableCode { .. } => RuleId::UnreachableCode, LintMessage::InfiniteLoop => RuleId::InfiniteLoop, LintMessage::DuplicateMatchArm { .. } => RuleId::DuplicateMatchArm, + LintMessage::DuplicateImport { .. } => RuleId::DuplicateImport, LintMessage::DeprecatedFunctionCall { .. } => RuleId::DeprecatedFunctionCall, LintMessage::ShadowVariable { .. } => RuleId::ShadowVariable, LintMessage::MissingElseInExpr => RuleId::MissingElseInExpr, @@ -268,6 +275,9 @@ impl LintMessage { LintMessage::DuplicateMatchArm { .. } => { Some("remove or merge this arm with the earlier identical pattern".to_string()) } + LintMessage::DuplicateImport { .. } => { + Some("remove or merge this import with the earlier identical pattern".to_string()) + } LintMessage::DeprecatedFunctionCall { name } => Some(format!( "deprecated function `{name}`; consider using an alternative or removing the call" )), @@ -351,6 +361,7 @@ impl fmt::Display for LintMessage { LintMessage::UnreachableCode { keyword } => write!(f, "unreachable code after `{keyword}`"), LintMessage::InfiniteLoop => write!(f, "loop without `break` may run forever"), LintMessage::DuplicateMatchArm { pattern } => write!(f, "duplicate match arm pattern `{pattern}`"), + LintMessage::DuplicateImport { name } => write!(f, "duplicate import of module `{name}`"), LintMessage::DeprecatedFunctionCall { name } => { write!(f, "call to deprecated function `{name}`") } diff --git a/crates/mq-lint/src/rules/correctness.rs b/crates/mq-lint/src/rules/correctness.rs index 39e3ccc62..4c1f48711 100644 --- a/crates/mq-lint/src/rules/correctness.rs +++ b/crates/mq-lint/src/rules/correctness.rs @@ -1,5 +1,6 @@ pub mod always_true_condition; pub mod deprecated_function_call; +pub mod duplicate_import; pub mod duplicate_match_arm; pub mod infinite_loop; pub mod missing_else_in_expr; @@ -20,6 +21,7 @@ pub fn all() -> Vec> { Box::new(infinite_loop::InfiniteLoop), Box::new(deprecated_function_call::DeprecatedFunctionCall), Box::new(duplicate_match_arm::DuplicateMatchArm), + Box::new(duplicate_import::DuplicateImport), Box::new(shadow_variable::ShadowVariable), Box::new(missing_else_in_expr::MissingElseInExpr), Box::new(always_true_condition::AlwaysTrueCondition), diff --git a/crates/mq-lint/src/rules/correctness/duplicate_import.rs b/crates/mq-lint/src/rules/correctness/duplicate_import.rs new file mode 100644 index 000000000..34e608b2d --- /dev/null +++ b/crates/mq-lint/src/rules/correctness/duplicate_import.rs @@ -0,0 +1,81 @@ +use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; +use rustc_hash::FxHashMap; + +pub struct DuplicateImport; + +impl LintRule for DuplicateImport { + fn id(&self) -> RuleId { + RuleId::DuplicateImport + } + + fn severity(&self) -> Severity { + Severity::Error + } + + fn check(&self, ctx: &LintContext<'_>) -> Vec { + let import_symbols = ctx + .all_symbols() + .filter_map(|(_, s)| if s.is_import() { Some(s) } else { None }) + .collect::>(); + let mut counts = FxHashMap::default(); + + for s in &import_symbols { + match &s.value { + Some(name) => { + counts.entry(name).or_insert((s, 0)).1 += 1; + } + None => { + // If the import has no name, we can skip it + continue; + } + } + } + + counts + .into_iter() + .filter(|(_, (_, count))| *count > 1) + .map(|(name, (s, _))| { + let mut d = Diagnostic::new(LintMessage::DuplicateImport { name: name.to_string() }, self.severity()); + + if let Some(range) = s.source.text_range { + d = d.with_range(range); + } + d + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use mq_hir::Hir; + + use super::*; + use crate::{LintConfig, LintContext}; + + fn check(code: &str) -> Vec { + let mut hir = Hir::default(); + let (source_id, _) = hir.add_code(None, code); + let config = LintConfig::default(); + let ctx = LintContext::new(&hir, source_id, &config); + DuplicateImport.check(&ctx) + } + + #[test] + fn detects_duplicate_import() { + let diags = check("import \"a\" | import \"a\" | a"); + assert_eq!(diags.len(), 1); + } + + #[test] + fn no_duplicate_import_with_other_module() { + let diags = check("import \"a\" | import \"b\" | a"); + assert_eq!(diags.len(), 0); + } + + #[test] + fn no_duplicate_import() { + let diags = check("import \"a\" | a"); + assert_eq!(diags.len(), 0); + } +}