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