use std::collections::HashMap;
use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{SymbolId, SymbolKind, SymbolPath};
use super::SpecSuggest;
use crate::lint::{LintDetails, LintSuggest};
use crate::{
LintSeverity, MutationSpec, OpportunityId, SafetyLevel, Suggest, SuggestCategory,
SuggestLocation, SuggestOpportunity, SuggestResult,
};
pub struct SpecGroupInconsistency {
spec_suffix: String,
}
impl SpecGroupInconsistency {
pub fn new() -> Self {
Self {
spec_suffix: "Spec".to_string(),
}
}
pub fn with_suffix(suffix: impl Into<String>) -> Self {
Self {
spec_suffix: suffix.into(),
}
}
fn extract_group_name(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Option<String> {
let typeflow = ctx.typeflow_graph();
for used_id in typeflow.types_used_by(symbol_id) {
if let Some(path) = ctx.registry.path(used_id) {
let name = path.name();
if name.ends_with("Group") {
return Some(name.to_string());
}
}
}
None
}
}
impl SpecSuggest for SpecGroupInconsistency {
fn spec_suffix(&self) -> &str {
&self.spec_suffix
}
}
impl Default for SpecGroupInconsistency {
fn default() -> Self {
Self::new()
}
}
impl Suggest for SpecGroupInconsistency {
fn name(&self) -> &'static str {
"spec-group-inconsistency"
}
fn description(&self) -> &str {
"Detects modules where Spec TypeAliases use different Groups"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Lint
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Manual }
fn priority_weight(&self) -> f32 {
1.5 }
fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
let mut opportunities = Vec::new();
let mut next_id = 0u32;
let mut module_specs: HashMap<SymbolPath, Vec<(SymbolId, String, Option<String>)>> =
HashMap::new();
let symbols_to_check: Vec<SymbolId> = if symbols.is_empty() {
ctx.registry.iter_by_kind(SymbolKind::TypeAlias).collect()
} else {
symbols.to_vec()
};
for symbol_id in symbols_to_check {
let path = match ctx.registry.path(symbol_id) {
Some(p) => p,
None => continue,
};
let alias_name = path.name();
if !self.is_spec_alias(alias_name) {
continue;
}
let module_path = match self.get_module_path(path) {
Some(mp) => mp,
None => continue,
};
let group_name = self.extract_group_name(ctx, symbol_id);
module_specs.entry(module_path).or_default().push((
symbol_id,
alias_name.to_string(),
group_name,
));
}
for (module_path, specs) in module_specs {
if specs.len() < 2 {
continue;
}
let groups: Vec<_> = specs.iter().filter_map(|(_, _, g)| g.as_ref()).collect();
let unique_groups: std::collections::HashSet<_> = groups.iter().collect();
if unique_groups.len() <= 1 {
continue; }
let mut group_counts: HashMap<&String, usize> = HashMap::new();
for g in &groups {
*group_counts.entry(g).or_insert(0) += 1;
}
let expected_group = group_counts
.iter()
.max_by_key(|(_, count)| *count)
.map(|(g, _)| (*g).clone());
for (symbol_id, alias_name, group_name) in &specs {
let spec_group = match group_name {
Some(g) => g,
None => continue,
};
if expected_group.as_ref() == Some(spec_group) {
continue; }
let Some(location) = SuggestLocation::from_context(ctx, *symbol_id) else {
continue;
};
let all_groups: Vec<_> = unique_groups.iter().map(|g| g.as_str()).collect();
let opp = self.create_lint_opportunity(
OpportunityId::new(next_id),
vec![*symbol_id],
location,
format!(
"Module `{}` has mixed Spec Groups: `{}` uses `{}` but module also has `{}`",
module_path,
alias_name,
spec_group,
expected_group.as_deref().unwrap_or("unknown")
),
LintDetails {
suggestion: Some(format!(
"Consider using `{}` for all Specs in this module",
expected_group.as_deref().unwrap_or("a consistent Group")
)),
expected: Some(format!(
"All Specs use `{}`",
expected_group.as_deref().unwrap_or("same Group")
)),
actual: Some(format!("Groups found: {}", all_groups.join(", "))),
},
);
opportunities.push(opp);
next_id += 1;
}
}
opportunities
}
fn to_mutation_specs(
&self,
_ctx: &AnalysisContext,
_opportunity: &SuggestOpportunity,
) -> SuggestResult<Vec<MutationSpec>> {
Ok(Vec::new())
}
}
impl LintSuggest for SpecGroupInconsistency {
fn code(&self) -> &'static str {
"RS004"
}
fn default_severity(&self) -> LintSeverity {
LintSeverity::Warning
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_spec_alias() {
let rule = SpecGroupInconsistency::new();
assert!(rule.is_spec_alias("TaskSpec"));
assert!(rule.is_spec_alias("UserSpec"));
assert!(!rule.is_spec_alias("Spec"));
assert!(!rule.is_spec_alias("Task"));
}
#[test]
fn test_custom_suffix() {
let rule = SpecGroupInconsistency::with_suffix("Domain");
assert!(rule.is_spec_alias("TaskDomain"));
assert!(!rule.is_spec_alias("TaskSpec"));
}
}