Skip to main content

cairo_lang_semantic/items/
feature_kind.rs

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