use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{SymbolId, SymbolKind};
use super::{is_framework_type, SpecSuggest};
use crate::lint::{LintDetails, LintSuggest};
use crate::{
LintSeverity, MutationSpec, OpportunityId, SafetyLevel, Suggest, SuggestCategory,
SuggestLocation, SuggestOpportunity, SuggestResult,
};
pub struct InvalidSpecRelation {
spec_suffix: String,
}
impl InvalidSpecRelation {
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 type_exists(&self, ctx: &AnalysisContext, type_name: &str) -> bool {
for (id, path) in ctx.registry.iter() {
let kind = ctx.registry.kind(id);
if matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum))
&& path.name() == type_name
{
return true;
}
}
false
}
fn extract_referenced_types(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Vec<String> {
let mut referenced_types = Vec::new();
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 kind = ctx.registry.kind(used_id);
if matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum)) {
referenced_types.push(path.name().to_string());
}
}
}
referenced_types
}
fn validate_base_type(&self, ctx: &AnalysisContext, alias_name: &str) -> Option<String> {
let base_type = self.extract_base_type(alias_name)?;
if !self.type_exists(ctx, base_type) {
Some(base_type.to_string())
} else {
None
}
}
}
impl SpecSuggest for InvalidSpecRelation {
fn spec_suffix(&self) -> &str {
&self.spec_suffix
}
}
impl Default for InvalidSpecRelation {
fn default() -> Self {
Self::new()
}
}
impl Suggest for InvalidSpecRelation {
fn name(&self) -> &'static str {
"invalid-spec-relation"
}
fn description(&self) -> &str {
"Detects SpecRelation targets that don't exist in the codebase"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Lint
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Manual }
fn priority_weight(&self) -> f32 {
2.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 let Some(missing_base) = self.validate_base_type(ctx, alias_name) {
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 `{}` references non-existent type `{}`",
alias_name, missing_base
),
LintDetails {
suggestion: Some(format!(
"Create type `{}` or update the Spec definition",
missing_base
)),
expected: Some(format!("Type `{}` exists", missing_base)),
actual: Some(format!("Type `{}` not found", missing_base)),
},
);
opportunities.push(opp);
next_id += 1;
continue; }
let referenced = self.extract_referenced_types(ctx, symbol_id);
for ref_type in &referenced {
if let Some(base) = self.extract_base_type(alias_name) {
if ref_type == base {
continue;
}
}
if is_framework_type(ref_type) {
continue;
}
if !self.type_exists(ctx, ref_type) {
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 `{}` has relation to non-existent type `{}`",
alias_name, ref_type
),
LintDetails {
suggestion: Some(format!(
"Create type `{}` or remove the relation",
ref_type
)),
expected: Some(format!("Type `{}` exists", ref_type)),
actual: Some(format!("Type `{}` not found", ref_type)),
},
);
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 _module_path = self.get_module_path(path);
Ok(vec![MutationSpec::ValidateSpec {
type_ids: vec![symbol_id],
expected_group: None,
validate_relations: true,
}])
}
}
impl LintSuggest for InvalidSpecRelation {
fn code(&self) -> &'static str {
"RS003"
}
fn default_severity(&self) -> LintSeverity {
LintSeverity::Error }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_spec_alias() {
let rule = InvalidSpecRelation::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_extract_base_type() {
let rule = InvalidSpecRelation::new();
assert_eq!(rule.extract_base_type("TaskSpec"), Some("Task"));
assert_eq!(rule.extract_base_type("UserSpec"), Some("User"));
assert_eq!(rule.extract_base_type("Spec"), None);
assert_eq!(rule.extract_base_type("Task"), None);
}
#[test]
fn test_custom_suffix() {
let rule = InvalidSpecRelation::with_suffix("Domain");
assert!(rule.is_spec_alias("TaskDomain"));
assert!(!rule.is_spec_alias("TaskSpec"));
assert_eq!(rule.extract_base_type("TaskDomain"), Some("Task"));
}
#[test]
fn test_is_framework_type() {
assert!(is_framework_type("Spec"));
assert!(is_framework_type("DomainGroup"));
assert!(is_framework_type("String"));
assert!(is_framework_type("Vec"));
assert!(is_framework_type("u64"));
assert!(!is_framework_type("Task"));
assert!(!is_framework_type("User"));
assert!(!is_framework_type("MyCustomType"));
}
}