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