use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{SymbolId, SymbolKind};
use super::SpecSuggest;
use crate::lint::{LintDetails, LintSuggest};
use crate::{
LintSeverity, MutationSpec, OpportunityId, SafetyLevel, Suggest, SuggestCategory, SuggestError,
SuggestLocation, SuggestOpportunity, SuggestResult,
};
pub struct OrphanSpec {
spec_suffix: String,
}
impl OrphanSpec {
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 is_referenced(&self, ctx: &AnalysisContext, target_id: SymbolId, target_name: &str) -> bool {
let graph = ctx.code_graph();
if graph.reference_count(target_id) > 0 {
return true;
}
let typeflow = ctx.typeflow_graph();
if typeflow.type_users(target_id).next().is_some() {
return true;
}
for (id, path) in ctx.registry.iter() {
if id == target_id {
continue;
}
let path_str = path.to_string();
if path_str.contains(target_name) {
return true;
}
}
false
}
}
impl SpecSuggest for OrphanSpec {
fn spec_suffix(&self) -> &str {
&self.spec_suffix
}
}
impl Default for OrphanSpec {
fn default() -> Self {
Self::new()
}
}
impl Suggest for OrphanSpec {
fn name(&self) -> &'static str {
"orphan-spec"
}
fn description(&self) -> &str {
"Detects Spec TypeAliases that are defined but never used"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Refactor
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Confirm }
fn priority_weight(&self) -> f32 {
1.0 }
fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
let mut opportunities = Vec::new();
let mut next_id = 0u32;
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;
}
if self.is_referenced(ctx, symbol_id, alias_name) {
continue;
}
let Some(location) = SuggestLocation::from_context(ctx, symbol_id) else {
continue;
};
let opp = self.create_lint_opportunity(
OpportunityId::new(next_id),
vec![symbol_id],
location,
format!("Spec TypeAlias `{}` is never used", alias_name),
LintDetails {
suggestion: Some(format!("Remove unused `type {} = ...;`", alias_name)),
expected: Some("Referenced somewhere".to_string()),
actual: Some("No references found".to_string()),
},
);
opportunities.push(opp);
next_id += 1;
}
opportunities
}
fn to_mutation_specs(
&self,
ctx: &AnalysisContext,
opportunity: &SuggestOpportunity,
) -> SuggestResult<Vec<MutationSpec>> {
let symbol_id = match opportunity.targets.first() {
Some(id) => *id,
None => return Ok(Vec::new()),
};
let path = match ctx.registry.path(symbol_id) {
Some(p) => p,
None => return Ok(Vec::new()),
};
let _alias_name = path.name().to_string();
let module_path =
self.get_module_path(path)
.ok_or_else(|| SuggestError::ModulePathResolution {
path: path.to_string(),
})?;
let module_id = match ctx.registry.lookup(&module_path) {
Some(id) => id,
None => return Ok(Vec::new()),
};
Ok(vec![MutationSpec::RemoveSpec {
type_id: symbol_id,
module_id,
}])
}
}
impl LintSuggest for OrphanSpec {
fn code(&self) -> &'static str {
"RS002"
}
fn default_severity(&self) -> LintSeverity {
LintSeverity::Warning
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_spec_alias() {
let rule = OrphanSpec::new();
assert!(rule.is_spec_alias("TaskSpec"));
assert!(rule.is_spec_alias("UserSpec"));
assert!(rule.is_spec_alias("ConfigSpec"));
assert!(!rule.is_spec_alias("Spec")); assert!(!rule.is_spec_alias("Task"));
assert!(!rule.is_spec_alias("SpecTask"));
}
#[test]
fn test_custom_suffix() {
let rule = OrphanSpec::with_suffix("Domain");
assert!(rule.is_spec_alias("TaskDomain"));
assert!(rule.is_spec_alias("UserDomain"));
assert!(!rule.is_spec_alias("TaskSpec"));
}
}