use cairo_lang_defs::ids::{LanguageElementId, ModuleId};
use cairo_lang_filesystem::db::{FilesGroup, default_crate_settings};
use cairo_lang_filesystem::ids::{CrateId, SmolStrId};
use cairo_lang_syntax::attribute::consts::{
ALLOW_ATTR, DEPRECATED_ATTR, FEATURE_ATTR, INTERNAL_ATTR, UNSTABLE_ATTR, UNUSED,
UNUSED_IMPORTS, UNUSED_VARIABLES,
};
use cairo_lang_syntax::attribute::structured::{
self, AttributeArg, AttributeArgVariant, AttributeStructurize,
};
use cairo_lang_syntax::node::helpers::QueryAttrs;
use cairo_lang_syntax::node::{Terminal, TypedStablePtr, TypedSyntaxNode, ast};
use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
use cairo_lang_utils::try_extract_matches;
use itertools::Itertools;
use salsa::Database;
use crate::db::SemanticGroup;
use crate::diagnostic::{SemanticDiagnosticKind, SemanticDiagnostics, SemanticDiagnosticsBuilder};
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
pub enum FeatureKind<'db> {
Stable,
Unstable { feature: SmolStrId<'db>, note: Option<SmolStrId<'db>> },
Deprecated { feature: SmolStrId<'db>, note: Option<SmolStrId<'db>> },
Internal { feature: SmolStrId<'db>, note: Option<SmolStrId<'db>> },
}
impl<'db> FeatureKind<'db> {
pub fn from_ast(
db: &'db dyn Database,
diagnostics: &mut SemanticDiagnostics<'db>,
attrs: &ast::AttributeList<'db>,
) -> Self {
let unstable_attrs = attrs.query_attr(db, UNSTABLE_ATTR).collect_vec();
let deprecated_attrs = attrs.query_attr(db, DEPRECATED_ATTR).collect_vec();
let internal_attrs = attrs.query_attr(db, INTERNAL_ATTR).collect_vec();
if unstable_attrs.is_empty() && deprecated_attrs.is_empty() && internal_attrs.is_empty() {
return Self::Stable;
};
if unstable_attrs.len() + deprecated_attrs.len() + internal_attrs.len() > 1 {
add_diag(diagnostics, attrs.stable_ptr(db), FeatureMarkerDiagnostic::MultipleMarkers);
return Self::Stable;
}
if !unstable_attrs.is_empty() {
let attr = unstable_attrs.into_iter().next().unwrap().structurize(db);
let [feature, note, _] =
parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
feature.map(|feature| Self::Unstable { feature, note }).ok_or(attr)
} else if !deprecated_attrs.is_empty() {
let attr = deprecated_attrs.into_iter().next().unwrap().structurize(db);
let [feature, note, _] =
parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
feature.map(|feature| Self::Deprecated { feature, note }).ok_or(attr)
} else {
let attr = internal_attrs.into_iter().next().unwrap().structurize(db);
let [feature, note, _] =
parse_feature_attr(db, diagnostics, &attr, ["feature", "note", "since"]);
feature.map(|feature| Self::Internal { feature, note }).ok_or(attr)
}
.unwrap_or_else(|attr| {
add_diag(diagnostics, attr.stable_ptr, FeatureMarkerDiagnostic::MissingAllowFeature);
Self::Stable
})
}
}
pub trait HasFeatureKind<'db> {
fn feature_kind(&self) -> &FeatureKind<'db>;
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub enum FeatureMarkerDiagnostic {
MultipleMarkers,
MissingAllowFeature,
UnsupportedArgument,
DuplicatedArgument,
}
fn parse_feature_attr<'db, const EXTRA_ALLOWED: usize>(
db: &'db dyn Database,
diagnostics: &mut SemanticDiagnostics<'db>,
attr: &structured::Attribute<'db>,
allowed_args: [&str; EXTRA_ALLOWED],
) -> [Option<SmolStrId<'db>>; EXTRA_ALLOWED] {
let mut arg_values = std::array::from_fn(|_| None);
for AttributeArg { variant, arg, .. } in &attr.args {
let AttributeArgVariant::Named { value: ast::Expr::String(value), name } = variant else {
add_diag(diagnostics, arg.stable_ptr(db), FeatureMarkerDiagnostic::UnsupportedArgument);
continue;
};
let Some(i) = allowed_args.iter().position(|x| *x == name.text.long(db)) else {
add_diag(diagnostics, name.stable_ptr, FeatureMarkerDiagnostic::UnsupportedArgument);
continue;
};
if arg_values[i].is_some() {
add_diag(diagnostics, name.stable_ptr, FeatureMarkerDiagnostic::DuplicatedArgument);
} else {
arg_values[i] = Some(value.text(db));
}
}
arg_values
}
fn add_diag<'db>(
diagnostics: &mut SemanticDiagnostics<'db>,
stable_ptr: impl TypedStablePtr<'db>,
diagnostic: FeatureMarkerDiagnostic,
) {
diagnostics
.report(stable_ptr.untyped(), SemanticDiagnosticKind::FeatureMarkerDiagnostic(diagnostic));
}
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
pub struct FeatureConfig<'db> {
pub allowed_features: OrderedHashSet<SmolStrId<'db>>,
pub allowed_lints: OrderedHashSet<SmolStrId<'db>>,
}
impl<'db> FeatureConfig<'db> {
pub fn override_with(&mut self, other: Self) -> FeatureConfigRestore<'db> {
let mut restore = FeatureConfigRestore::empty();
for feature_name in other.allowed_features {
if self.allowed_features.insert(feature_name) {
restore.features_to_remove.push(feature_name);
}
}
for allow_lint in other.allowed_lints {
if self.allowed_lints.insert(allow_lint) {
restore.allowed_lints_to_remove.push(allow_lint);
}
}
restore
}
pub fn restore(&mut self, restore: FeatureConfigRestore<'db>) {
for feature_name in restore.features_to_remove {
self.allowed_features.swap_remove(&feature_name);
}
for allow_lint in restore.allowed_lints_to_remove {
self.allowed_lints.swap_remove(&allow_lint);
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct FeatureConfigRestore<'db> {
features_to_remove: Vec<SmolStrId<'db>>,
allowed_lints_to_remove: Vec<SmolStrId<'db>>,
}
impl FeatureConfigRestore<'_> {
pub fn empty() -> Self {
Self::default()
}
}
pub fn feature_config_from_ast_item<'db>(
db: &'db dyn Database,
crate_id: CrateId<'db>,
syntax: &impl QueryAttrs<'db>,
diagnostics: &mut SemanticDiagnostics<'db>,
) -> FeatureConfig<'db> {
let mut config = FeatureConfig::default();
process_feature_attr_kind(
db,
syntax,
FEATURE_ATTR,
|| SemanticDiagnosticKind::UnsupportedFeatureAttrArguments,
diagnostics,
|value| {
if let ast::Expr::String(value) = value {
config.allowed_features.insert(value.text(db));
true
} else {
false
}
},
);
process_feature_attr_kind(
db,
syntax,
ALLOW_ATTR,
|| SemanticDiagnosticKind::UnsupportedAllowAttrArguments,
diagnostics,
|value| {
let allowed = value.as_syntax_node().get_text_without_trivia(db);
if allowed.long(db) == UNUSED {
let all_unused_lints = [UNUSED_VARIABLES, UNUSED_IMPORTS];
return all_unused_lints.iter().all(|&lint| {
let _already_allowed = config.allowed_lints.insert(SmolStrId::from(db, lint));
db.declared_allows(crate_id).contains(lint)
});
}
let _already_allowed = config.allowed_lints.insert(allowed);
db.declared_allows(crate_id).contains(allowed.long(db).as_str())
},
);
config
}
fn process_feature_attr_kind<'db>(
db: &'db dyn Database,
syntax: &impl QueryAttrs<'db>,
attr: &'db str,
diagnostic_kind: impl Fn() -> SemanticDiagnosticKind<'db>,
diagnostics: &mut SemanticDiagnostics<'db>,
mut process: impl FnMut(&ast::Expr<'db>) -> bool,
) {
for attr_syntax in syntax.query_attr(db, attr) {
let attr = attr_syntax.structurize(db);
let success = (|| {
let [arg] = &attr.args[..] else {
return None;
};
let value = try_extract_matches!(&arg.variant, AttributeArgVariant::Unnamed)?;
process(value).then_some(())
})()
.is_none();
if success {
diagnostics.report(attr.args_stable_ptr.untyped(), diagnostic_kind());
}
}
}
pub fn feature_config_from_item_and_parent_modules<'db>(
db: &'db dyn Database,
element_id: &impl LanguageElementId<'db>,
syntax: &impl QueryAttrs<'db>,
diagnostics: &mut SemanticDiagnostics<'db>,
) -> FeatureConfig<'db> {
let mut current_module_id = element_id.parent_module(db);
let crate_id = current_module_id.owning_crate(db);
let mut config_stack = vec![feature_config_from_ast_item(db, crate_id, syntax, diagnostics)];
let mut config = loop {
match current_module_id {
ModuleId::CrateRoot(crate_id) => {
let all_declarations_pub = db
.crate_config(crate_id)
.map(|config| config.settings.edition.ignore_visibility())
.unwrap_or_else(|| default_crate_settings(db).edition.ignore_visibility());
let mut allowed_lints = OrderedHashSet::default();
if all_declarations_pub {
let _already_allowed =
allowed_lints.insert(SmolStrId::from(db, UNUSED_IMPORTS));
}
break FeatureConfig { allowed_features: OrderedHashSet::default(), allowed_lints };
}
ModuleId::Submodule(id) => {
current_module_id = id.parent_module(db);
let module = ¤t_module_id.module_data(db).unwrap().submodules(db)[&id];
let ignored = &mut SemanticDiagnostics::new(current_module_id);
config_stack.push(feature_config_from_ast_item(db, crate_id, module, ignored));
}
ModuleId::MacroCall { id, .. } => {
current_module_id = id.parent_module(db);
}
}
};
for module_config in config_stack.into_iter().rev() {
config.override_with(module_config);
}
config
}