cairo_lang_semantic/items/
feature_kind.rs

1use cairo_lang_defs::diagnostic_utils::StableLocation;
2use cairo_lang_defs::ids::{LanguageElementId, ModuleId};
3use cairo_lang_diagnostics::DiagnosticsBuilder;
4use cairo_lang_filesystem::ids::CrateId;
5use cairo_lang_syntax::attribute::consts::{
6    ALLOW_ATTR, DEPRECATED_ATTR, FEATURE_ATTR, INTERNAL_ATTR, UNSTABLE_ATTR,
7};
8use cairo_lang_syntax::attribute::structured::{
9    self, AttributeArg, AttributeArgVariant, AttributeStructurize,
10};
11use cairo_lang_syntax::node::db::SyntaxGroup;
12use cairo_lang_syntax::node::helpers::QueryAttrs;
13use cairo_lang_syntax::node::{Terminal, TypedStablePtr, TypedSyntaxNode, ast};
14use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
15use cairo_lang_utils::try_extract_matches;
16use smol_str::SmolStr;
17
18use crate::SemanticDiagnostic;
19use crate::db::SemanticGroup;
20use crate::diagnostic::{SemanticDiagnosticKind, SemanticDiagnostics, SemanticDiagnosticsBuilder};
21
22/// The kind of a feature for an item.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum FeatureKind {
25    /// The feature of the item is stable.
26    Stable,
27    /// The feature of the item is unstable, with the given name to allow.
28    Unstable { feature: SmolStr, note: Option<SmolStr> },
29    /// The feature of the item is deprecated, with the given name to allow, and an optional note
30    /// to appear in diagnostics.
31    Deprecated { feature: SmolStr, note: Option<SmolStr> },
32    /// This feature is for internal corelib use only. Using it in user code is not advised.
33    Internal { feature: SmolStr, note: Option<SmolStr> },
34}
35impl FeatureKind {
36    pub fn from_ast(
37        db: &dyn SyntaxGroup,
38        diagnostics: &mut DiagnosticsBuilder<SemanticDiagnostic>,
39        attrs: &ast::AttributeList,
40    ) -> Self {
41        let unstable_attrs = attrs.query_attr(db, UNSTABLE_ATTR);
42        let deprecated_attrs = attrs.query_attr(db, DEPRECATED_ATTR);
43        let internal_attrs = attrs.query_attr(db, INTERNAL_ATTR);
44        if unstable_attrs.is_empty() && deprecated_attrs.is_empty() && internal_attrs.is_empty() {
45            return Self::Stable;
46        };
47        if unstable_attrs.len() + deprecated_attrs.len() + internal_attrs.len() > 1 {
48            add_diag(diagnostics, &attrs.stable_ptr(), FeatureMarkerDiagnostic::MultipleMarkers);
49            return Self::Stable;
50        }
51
52        if !unstable_attrs.is_empty() {
53            let attr = unstable_attrs.into_iter().next().unwrap().structurize(db);
54            let [feature, note, _] =
55                parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
56            feature.map(|feature| Self::Unstable { feature, note }).ok_or(attr)
57        } else if !deprecated_attrs.is_empty() {
58            let attr = deprecated_attrs.into_iter().next().unwrap().structurize(db);
59            let [feature, note, _] =
60                parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
61            feature.map(|feature| Self::Deprecated { feature, note }).ok_or(attr)
62        } else {
63            let attr = internal_attrs.into_iter().next().unwrap().structurize(db);
64            let [feature, note, _] =
65                parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
66            feature.map(|feature| Self::Internal { feature, note }).ok_or(attr)
67        }
68        .unwrap_or_else(|attr| {
69            add_diag(diagnostics, &attr.stable_ptr, FeatureMarkerDiagnostic::MissingAllowFeature);
70            Self::Stable
71        })
72    }
73}
74
75/// A trait for retrieving the `FeatureKind` of an item.
76pub trait HasFeatureKind {
77    /// Returns the `FeatureKind` associated with the implementing struct.
78    fn feature_kind(&self) -> &FeatureKind;
79}
80
81/// Diagnostics for feature markers.
82#[derive(Clone, Debug, Eq, Hash, PartialEq)]
83pub enum FeatureMarkerDiagnostic {
84    /// Multiple markers on the same item.
85    MultipleMarkers,
86    /// Every marker must have a feature argument, to allow ignoring the warning.
87    MissingAllowFeature,
88    /// Unsupported argument in the feature marker attribute.
89    UnsupportedArgument,
90    /// Duplicated argument in the feature marker attribute.
91    DuplicatedArgument,
92}
93
94/// Parses the feature attribute.
95fn parse_feature_attr<const EXTRA_ALLOWED: usize>(
96    db: &dyn SyntaxGroup,
97    diagnostics: &mut DiagnosticsBuilder<SemanticDiagnostic>,
98    attr: &structured::Attribute,
99    allowed_args: [&str; EXTRA_ALLOWED],
100) -> [Option<SmolStr>; EXTRA_ALLOWED] {
101    let mut arg_values = std::array::from_fn(|_| None);
102    for AttributeArg { variant, arg, .. } in &attr.args {
103        let AttributeArgVariant::Named { value: ast::Expr::String(value), name } = variant else {
104            add_diag(diagnostics, &arg.stable_ptr(), FeatureMarkerDiagnostic::UnsupportedArgument);
105            continue;
106        };
107        let Some(i) = allowed_args.iter().position(|x| x == &name.text.as_str()) else {
108            add_diag(diagnostics, &name.stable_ptr, FeatureMarkerDiagnostic::UnsupportedArgument);
109            continue;
110        };
111        if arg_values[i].is_some() {
112            add_diag(diagnostics, &name.stable_ptr, FeatureMarkerDiagnostic::DuplicatedArgument);
113        } else {
114            arg_values[i] = Some(value.text(db));
115        }
116    }
117    arg_values
118}
119
120/// Helper for adding a marker diagnostic.
121fn add_diag(
122    diagnostics: &mut DiagnosticsBuilder<SemanticDiagnostic>,
123    stable_ptr: &impl TypedStablePtr,
124    diagnostic: FeatureMarkerDiagnostic,
125) {
126    diagnostics.add(SemanticDiagnostic::new(
127        StableLocation::new(stable_ptr.untyped()),
128        SemanticDiagnosticKind::FeatureMarkerDiagnostic(diagnostic),
129    ));
130}
131
132/// The feature configuration on an item.
133/// May be accumulated, or overridden by inner items.
134#[derive(Clone, Debug, Default, PartialEq, Eq)]
135pub struct FeatureConfig {
136    /// The current set of allowed features.
137    pub allowed_features: OrderedHashSet<SmolStr>,
138    /// Whether to allow all deprecated features.
139    pub allow_deprecated: bool,
140    /// Whether to allow unused imports.
141    pub allow_unused_imports: bool,
142}
143
144impl FeatureConfig {
145    /// Overrides the current configuration with another one.
146    ///
147    /// Returns the data required to restore the configuration.
148    pub fn override_with(&mut self, other: Self) -> FeatureConfigRestore {
149        let mut restore = FeatureConfigRestore {
150            features_to_remove: vec![],
151            allow_deprecated: self.allow_deprecated,
152            allow_unused_imports: self.allow_unused_imports,
153        };
154        for feature_name in other.allowed_features {
155            if self.allowed_features.insert(feature_name.clone()) {
156                restore.features_to_remove.push(feature_name);
157            }
158        }
159        self.allow_deprecated |= other.allow_deprecated;
160        self.allow_unused_imports |= other.allow_unused_imports;
161        restore
162    }
163
164    /// Restores the configuration to a previous state.
165    pub fn restore(&mut self, restore: FeatureConfigRestore) {
166        for feature_name in restore.features_to_remove {
167            self.allowed_features.swap_remove(&feature_name);
168        }
169        self.allow_deprecated = restore.allow_deprecated;
170        self.allow_unused_imports = restore.allow_unused_imports;
171    }
172}
173
174/// The data required to restore the feature configuration after an override.
175pub struct FeatureConfigRestore {
176    /// The features to remove from the configuration after the override.
177    features_to_remove: Vec<SmolStr>,
178    /// The previous state of the allow deprecated flag.
179    allow_deprecated: bool,
180    /// The previous state of the allow unused imports flag.
181    allow_unused_imports: bool,
182}
183
184/// Returns the allowed features of an object which supports attributes.
185pub fn extract_item_feature_config(
186    db: &dyn SemanticGroup,
187    crate_id: CrateId,
188    syntax: &impl QueryAttrs,
189    diagnostics: &mut SemanticDiagnostics,
190) -> FeatureConfig {
191    let syntax_db = db.upcast();
192    let mut config = FeatureConfig::default();
193    process_feature_attr_kind(
194        syntax_db,
195        syntax,
196        FEATURE_ATTR,
197        || SemanticDiagnosticKind::UnsupportedFeatureAttrArguments,
198        diagnostics,
199        |value| {
200            if let ast::Expr::String(value) = value {
201                config.allowed_features.insert(value.text(syntax_db));
202                true
203            } else {
204                false
205            }
206        },
207    );
208    process_feature_attr_kind(
209        syntax_db,
210        syntax,
211        ALLOW_ATTR,
212        || SemanticDiagnosticKind::UnsupportedAllowAttrArguments,
213        diagnostics,
214        |value| match value.as_syntax_node().get_text_without_trivia(syntax_db).as_str() {
215            "deprecated" => {
216                config.allow_deprecated = true;
217                true
218            }
219            "unused_imports" => {
220                config.allow_unused_imports = true;
221                true
222            }
223            other => db.declared_allows(crate_id).contains(other),
224        },
225    );
226    config
227}
228
229/// Processes the feature attribute kind.
230fn process_feature_attr_kind(
231    db: &dyn SyntaxGroup,
232    syntax: &impl QueryAttrs,
233    attr: &str,
234    diagnostic_kind: impl Fn() -> SemanticDiagnosticKind,
235    diagnostics: &mut SemanticDiagnostics,
236    mut process: impl FnMut(&ast::Expr) -> bool,
237) {
238    for attr_syntax in syntax.query_attr(db, attr) {
239        let attr = attr_syntax.structurize(db);
240        let success = (|| {
241            let [arg] = &attr.args[..] else {
242                return None;
243            };
244            let value = try_extract_matches!(&arg.variant, AttributeArgVariant::Unnamed)?;
245            process(value).then_some(())
246        })()
247        .is_none();
248        if success {
249            diagnostics.report(attr.args_stable_ptr.untyped(), diagnostic_kind());
250        }
251    }
252}
253
254/// Extracts the allowed features of an element, considering its parent modules as well as its
255/// attributes.
256pub fn extract_feature_config(
257    db: &dyn SemanticGroup,
258    element_id: &impl LanguageElementId,
259    syntax: &impl QueryAttrs,
260    diagnostics: &mut SemanticDiagnostics,
261) -> FeatureConfig {
262    let defs_db = db.upcast();
263    let mut current_module_id = element_id.parent_module(defs_db);
264    let crate_id = current_module_id.owning_crate(defs_db);
265    let mut config_stack = vec![extract_item_feature_config(db, crate_id, syntax, diagnostics)];
266    let mut config = loop {
267        match current_module_id {
268            ModuleId::CrateRoot(crate_id) => {
269                let settings =
270                    db.crate_config(crate_id).map(|config| config.settings).unwrap_or_default();
271                break FeatureConfig {
272                    allowed_features: OrderedHashSet::default(),
273                    allow_deprecated: false,
274                    allow_unused_imports: settings.edition.ignore_visibility(),
275                };
276            }
277            ModuleId::Submodule(id) => {
278                current_module_id = id.parent_module(defs_db);
279                let module = &db.module_submodules(current_module_id).unwrap()[&id];
280                // TODO(orizi): Add parent module diagnostics.
281                let ignored = &mut SemanticDiagnostics::default();
282                config_stack.push(extract_item_feature_config(db, crate_id, module, ignored));
283            }
284        }
285    };
286    for module_config in config_stack.into_iter().rev() {
287        config.override_with(module_config);
288    }
289    config
290}