Skip to main content

allow_core/
finding.rs

1use crate::{CargoAllowError, normalize_path};
2use std::fmt;
3use std::path::PathBuf;
4use std::str::FromStr;
5
6pub const STRUCTURAL_IDENTITY_SCHEMA_ID: &str = "cargo-allow.structural-identity.v1";
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Span {
10    pub line: u32,
11    pub column: u32,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum FindingKind {
16    Panic,
17    Unsafe,
18    LintException,
19    NonRustFile,
20    GeneratedCode,
21    PolicyException,
22}
23
24impl FindingKind {
25    pub const ALL: &[Self] = &[
26        Self::Panic,
27        Self::Unsafe,
28        Self::LintException,
29        Self::NonRustFile,
30        Self::GeneratedCode,
31        Self::PolicyException,
32    ];
33
34    pub fn as_str(self) -> &'static str {
35        match self {
36            Self::Panic => "panic",
37            Self::Unsafe => "unsafe",
38            Self::LintException => "lint_exception",
39            Self::NonRustFile => "non_rust_file",
40            Self::GeneratedCode => "generated_code",
41            Self::PolicyException => "policy_exception",
42        }
43    }
44
45    pub fn requires_source_selector_identity(self) -> bool {
46        matches!(self, Self::Panic | Self::Unsafe | Self::LintException)
47    }
48}
49
50impl fmt::Display for FindingKind {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{}", self.as_str())
53    }
54}
55
56impl FromStr for FindingKind {
57    type Err = CargoAllowError;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        match s.trim() {
61            "panic" | "panic_family" | "panic-family" | "indexing" => Ok(Self::Panic),
62            "unsafe" => Ok(Self::Unsafe),
63            "lint_exception" | "lint-exception" | "clippy" | "allow_attribute"
64            | "allow-attribute" | "expect_attribute" | "expect-attribute" => {
65                Ok(Self::LintException)
66            }
67            "non_rust_file" | "non-rust-file" | "non_rust" | "non-rust" | "file" => {
68                Ok(Self::NonRustFile)
69            }
70            "generated_code" | "generated-code" | "generated" => Ok(Self::GeneratedCode),
71            "policy_exception" | "policy-exception" | "policy" => Ok(Self::PolicyException),
72            other => Err(CargoAllowError::new(format!(
73                "unsupported finding kind `{other}`"
74            ))),
75        }
76    }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct StructuralIdentity {
81    pub language: String,
82    pub crate_name: Option<String>,
83    pub module: Option<String>,
84    pub container: Option<String>,
85    pub ast_kind: String,
86    pub symbol: Option<String>,
87    pub callee: Option<String>,
88    pub macro_name: Option<String>,
89    pub lint: Option<String>,
90    pub receiver_fingerprint: Option<String>,
91    pub target_fingerprint: Option<String>,
92    pub normalized_snippet_hash: Option<String>,
93    pub line_hint: Option<u32>,
94    pub column_hint: Option<u32>,
95}
96
97impl StructuralIdentity {
98    pub fn schema_id() -> &'static str {
99        STRUCTURAL_IDENTITY_SCHEMA_ID
100    }
101
102    pub fn new(language: impl Into<String>, ast_kind: impl Into<String>) -> Self {
103        Self {
104            language: language.into(),
105            crate_name: None,
106            module: None,
107            container: None,
108            ast_kind: ast_kind.into(),
109            symbol: None,
110            callee: None,
111            macro_name: None,
112            lint: None,
113            receiver_fingerprint: None,
114            target_fingerprint: None,
115            normalized_snippet_hash: None,
116            line_hint: None,
117            column_hint: None,
118        }
119    }
120
121    pub fn stable_key(&self) -> String {
122        stable_identity_key_from_parts(self.stable_key_parts())
123    }
124
125    pub fn stable_key_parts(&self) -> Vec<(&'static str, String)> {
126        vec![
127            ("language", self.language.clone()),
128            ("crate_name", self.crate_name.clone().unwrap_or_default()),
129            ("module", self.module.clone().unwrap_or_default()),
130            ("container", self.container.clone().unwrap_or_default()),
131            ("ast_kind", self.ast_kind.clone()),
132            ("symbol", self.symbol.clone().unwrap_or_default()),
133            ("callee", self.callee.clone().unwrap_or_default()),
134            ("macro_name", self.macro_name.clone().unwrap_or_default()),
135            ("lint", self.lint.clone().unwrap_or_default()),
136            (
137                "receiver_fingerprint",
138                self.receiver_fingerprint.clone().unwrap_or_default(),
139            ),
140            (
141                "target_fingerprint",
142                self.target_fingerprint.clone().unwrap_or_default(),
143            ),
144            (
145                "normalized_snippet_hash",
146                self.normalized_snippet_hash.clone().unwrap_or_default(),
147            ),
148        ]
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct Finding {
154    pub kind: FindingKind,
155    pub family: Option<String>,
156    pub path: PathBuf,
157    pub span: Option<Span>,
158    pub identity: StructuralIdentity,
159    pub message: String,
160}
161
162impl Finding {
163    pub fn source_package_name(&self) -> Option<&str> {
164        self.identity
165            .crate_name
166            .as_deref()
167            .map(str::trim)
168            .filter(|name| !name.is_empty())
169    }
170}
171
172pub fn finding_identity_key(finding: &Finding) -> String {
173    let mut parts = vec![
174        ("kind", finding.kind.as_str().to_string()),
175        ("family", finding.family.clone().unwrap_or_default()),
176        ("path", normalize_path(&finding.path)),
177    ];
178    parts.extend(finding.identity.stable_key_parts());
179    stable_identity_key_from_parts(parts)
180}
181
182fn stable_identity_key_from_parts(parts: Vec<(&'static str, String)>) -> String {
183    parts
184        .into_iter()
185        .map(|(name, value)| format!("{name}:{}:{value}", value.len()))
186        .collect::<Vec<_>>()
187        .join("|")
188}
189
190#[cfg(test)]
191#[path = "finding_tests.rs"]
192mod tests;