cairo_lang_semantic/items/
feature_kind.rs1use 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#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
26pub enum FeatureKind<'db> {
27 Stable,
29 Unstable { feature: SmolStrId<'db>, note: Option<SmolStrId<'db>> },
31 Deprecated { feature: SmolStrId<'db>, note: Option<SmolStrId<'db>> },
34 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
77pub trait HasFeatureKind<'db> {
79 fn feature_kind(&self) -> &FeatureKind<'db>;
81}
82
83#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
85pub enum FeatureMarkerDiagnostic {
86 MultipleMarkers,
88 MissingAllowFeature,
90 UnsupportedArgument,
92 DuplicatedArgument,
94}
95
96fn 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
122fn 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#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
137pub struct FeatureConfig<'db> {
138 pub allowed_features: OrderedHashSet<SmolStrId<'db>>,
140 pub allowed_lints: OrderedHashSet<SmolStrId<'db>>,
142}
143
144impl<'db> FeatureConfig<'db> {
145 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 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#[derive(Debug, Default, PartialEq, Eq)]
176pub struct FeatureConfigRestore<'db> {
177 features_to_remove: Vec<SmolStrId<'db>>,
179 allowed_lints_to_remove: Vec<SmolStrId<'db>>,
181}
182
183impl FeatureConfigRestore<'_> {
184 pub fn empty() -> Self {
185 Self::default()
186 }
187}
188
189pub 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 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
236fn 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
261pub 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 = ¤t_module_id.module_data(db).unwrap().submodules(db)[&id];
289 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}