Skip to main content

fallow_config/config/
used_class_members.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4/// A `usedClassMembers` entry from config or an external plugin.
5///
6/// Supports either a plain member name (`"agInit"`) or a scoped rule that
7/// only applies when a class matches specific `extends` / `implements`
8/// heritage clauses.
9#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
10#[serde(untagged)]
11pub enum UsedClassMemberRule {
12    /// Globally suppress this class member name for all classes.
13    Name(String),
14    /// Suppress these class member names only for matching classes.
15    Scoped(ScopedUsedClassMemberRule),
16}
17
18impl From<&str> for UsedClassMemberRule {
19    fn from(value: &str) -> Self {
20        Self::Name(value.to_string())
21    }
22}
23
24impl From<String> for UsedClassMemberRule {
25    fn from(value: String) -> Self {
26        Self::Name(value)
27    }
28}
29
30/// A heritage-constrained `usedClassMembers` rule.
31#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
32#[serde(rename_all = "camelCase", deny_unknown_fields)]
33pub struct ScopedUsedClassMemberRule {
34    /// Only apply when the class extends this parent class name.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub extends: Option<String>,
37    /// Only apply when the class implements this interface name.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub implements: Option<String>,
40    /// Member names that should be treated as framework-used.
41    pub members: Vec<String>,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46struct ScopedUsedClassMemberRuleDef {
47    #[serde(default)]
48    extends: Option<String>,
49    #[serde(default)]
50    implements: Option<String>,
51    members: Vec<String>,
52}
53
54impl TryFrom<ScopedUsedClassMemberRuleDef> for ScopedUsedClassMemberRule {
55    type Error = &'static str;
56
57    fn try_from(value: ScopedUsedClassMemberRuleDef) -> Result<Self, Self::Error> {
58        if value.extends.is_none() && value.implements.is_none() {
59            return Err("scoped usedClassMembers rules require `extends` or `implements`");
60        }
61
62        Ok(Self {
63            extends: value.extends,
64            implements: value.implements,
65            members: value.members,
66        })
67    }
68}
69
70impl<'de> Deserialize<'de> for ScopedUsedClassMemberRule {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: serde::Deserializer<'de>,
74    {
75        ScopedUsedClassMemberRuleDef::deserialize(deserializer)?
76            .try_into()
77            .map_err(serde::de::Error::custom)
78    }
79}
80
81impl ScopedUsedClassMemberRule {
82    #[must_use]
83    pub fn matches_heritage(
84        &self,
85        super_class: Option<&str>,
86        implemented_interfaces: &[String],
87    ) -> bool {
88        let extends_matches = self
89            .extends
90            .as_deref()
91            .is_none_or(|expected| super_class == Some(expected));
92        let implements_matches = self
93            .implements
94            .as_deref()
95            .is_none_or(|expected| implemented_interfaces.iter().any(|iface| iface == expected));
96
97        extends_matches && implements_matches
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn deserialize_plain_member_name() {
107        let rule: UsedClassMemberRule = serde_json::from_str(r#""agInit""#).unwrap();
108        assert_eq!(rule, UsedClassMemberRule::Name("agInit".to_string()));
109    }
110
111    #[test]
112    fn deserialize_scoped_rule() {
113        let rule: UsedClassMemberRule = serde_json::from_str(
114            r#"{"implements":"ICellRendererAngularComp","members":["refresh"]}"#,
115        )
116        .unwrap();
117        assert_eq!(
118            rule,
119            UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
120                extends: None,
121                implements: Some("ICellRendererAngularComp".to_string()),
122                members: vec!["refresh".to_string()],
123            })
124        );
125    }
126
127    #[test]
128    fn scoped_rule_matches_extends_and_implements() {
129        let rule = ScopedUsedClassMemberRule {
130            extends: Some("BaseCommand".to_string()),
131            implements: Some("Runnable".to_string()),
132            members: vec!["execute".to_string()],
133        };
134
135        assert!(rule.matches_heritage(
136            Some("BaseCommand"),
137            &["Runnable".to_string(), "Disposable".to_string()]
138        ));
139        assert!(!rule.matches_heritage(Some("OtherBase"), &["Runnable".to_string()]));
140        assert!(!rule.matches_heritage(Some("BaseCommand"), &["Other".to_string()]));
141    }
142
143    #[test]
144    fn deserialize_scoped_rule_requires_constraint() {
145        let error = serde_json::from_str::<ScopedUsedClassMemberRule>(r#"{"members":["refresh"]}"#)
146            .unwrap_err()
147            .to_string();
148        assert!(
149            error.contains("require `extends` or `implements`"),
150            "unexpected error: {error}"
151        );
152    }
153}