Skip to main content

rscheck_cli/rules/public_api_errors/
mod.rs

1use crate::analysis::Workspace;
2use crate::config::PublicApiErrorsConfig;
3use crate::emit::Emitter;
4use crate::path_pattern::matches_path_prefix;
5use crate::report::Finding;
6use crate::rules::{Rule, RuleBackend, RuleContext, RuleFamily, RuleInfo};
7use crate::span::Span;
8use quote::ToTokens;
9
10pub struct PublicApiErrorsRule;
11
12impl PublicApiErrorsRule {
13    pub fn static_info() -> RuleInfo {
14        RuleInfo {
15            id: "design.public_api_errors",
16            family: RuleFamily::Design,
17            backend: RuleBackend::Syntax,
18            summary: "Checks public Result error types against an allow-list.",
19            default_level: PublicApiErrorsConfig::default().level,
20            schema: "level, allowed_error_types",
21            config_example: "[rules.\"design.public_api_errors\"]\nlevel = \"deny\"\nallowed_error_types = [\"crate::Error\"]",
22            fixable: false,
23        }
24    }
25}
26
27impl Rule for PublicApiErrorsRule {
28    fn info(&self) -> RuleInfo {
29        Self::static_info()
30    }
31
32    fn run(&self, ws: &Workspace, ctx: &RuleContext<'_>, out: &mut dyn Emitter) {
33        for file in &ws.files {
34            let cfg = match ctx
35                .policy
36                .decode_rule::<PublicApiErrorsConfig>(Self::static_info().id, Some(&file.path))
37            {
38                Ok(cfg) => cfg,
39                Err(_) => continue,
40            };
41            if !cfg.level.enabled() {
42                continue;
43            }
44            let Some(ast) = &file.ast else { continue };
45            for item in &ast.items {
46                let syn::Item::Fn(item_fn) = item else {
47                    continue;
48                };
49                if !matches!(item_fn.vis, syn::Visibility::Public(_)) {
50                    continue;
51                }
52                let syn::ReturnType::Type(_, ty) = &item_fn.sig.output else {
53                    continue;
54                };
55                let Some(error_ty) = extract_result_error_type(ty) else {
56                    continue;
57                };
58                if cfg
59                    .allowed_error_types
60                    .iter()
61                    .any(|allowed| matches_path_prefix(&error_ty, allowed))
62                {
63                    continue;
64                }
65                out.emit(Finding {
66                    rule_id: Self::static_info().id.to_string(),
67                    family: Some(Self::static_info().family),
68                    engine: Some(Self::static_info().backend),
69                    severity: cfg.level.to_severity(),
70                    message: format!(
71                        "public API returns disallowed error type `{error_ty}` in `{}`",
72                        item_fn.sig.ident
73                    ),
74                    primary: Some(Span::from_pm_span(&file.path, item_fn.sig.ident.span())),
75                    secondary: Vec::new(),
76                    help: Some("Return an approved error type from this public API.".to_string()),
77                    evidence: None,
78                    confidence: None,
79                    tags: vec!["api".to_string(), "errors".to_string()],
80                    labels: Vec::new(),
81                    notes: Vec::new(),
82                    fixes: Vec::new(),
83                });
84            }
85        }
86    }
87}
88
89fn extract_result_error_type(ty: &syn::Type) -> Option<String> {
90    let syn::Type::Path(path) = ty else {
91        return None;
92    };
93    let segment = path.path.segments.last()?;
94    if segment.ident != "Result" {
95        return None;
96    }
97    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
98        return None;
99    };
100    let mut ty_args = args.args.iter().filter_map(|arg| match arg {
101        syn::GenericArgument::Type(ty) => Some(ty),
102        _ => None,
103    });
104    let _ok = ty_args.next()?;
105    let err = ty_args.next()?;
106    Some(err.to_token_stream().to_string().replace(' ', ""))
107}
108
109#[cfg(test)]
110mod tests;