rscheck_cli/rules/public_api_errors/
mod.rs1use 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;