use std::fmt::Write as _;
use std::sync::Arc;
use cairo_lang_debug::debug::DebugWithDb;
use cairo_lang_filesystem::db::{CrateConfiguration, FilesGroup, init_files_group};
use cairo_lang_filesystem::ids::{CrateId, Directory, FileLongId, SmolStrId};
use cairo_lang_filesystem::{override_file_content, set_crate_config};
use cairo_lang_parser::db::ParserGroup;
use cairo_lang_syntax::node::helpers::QueryAttrs;
use cairo_lang_syntax::node::kind::SyntaxKind;
use cairo_lang_syntax::node::{SyntaxNode, Terminal, TypedSyntaxNode, ast};
use cairo_lang_test_utils::parse_test_file::TestRunnerResult;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use cairo_lang_utils::{Intern, extract_matches, try_extract_matches};
use indoc::indoc;
use salsa::{Database, Setter};
use crate::db::{DefsGroup, defs_group_input, init_defs_group, init_external_files};
use crate::ids::{
GenericParamLongId, MacroPluginLongId, ModuleId, ModuleItemId, NamedLanguageElementId,
SubmoduleLongId,
};
use crate::plugin::{
MacroPlugin, MacroPluginMetadata, PluginDiagnostic, PluginGeneratedFile, PluginResult,
};
#[salsa::db]
#[derive(Clone)]
pub struct DatabaseForTesting {
storage: salsa::Storage<DatabaseForTesting>,
}
#[salsa::db]
impl salsa::Database for DatabaseForTesting {}
impl Default for DatabaseForTesting {
fn default() -> Self {
let mut res = Self { storage: Default::default() };
init_external_files(&mut res);
init_files_group(&mut res);
init_defs_group(&mut res);
defs_group_input(&res).set_default_macro_plugins(&mut res).to(Some(vec![
MacroPluginLongId(Arc::new(FooToBarPlugin)),
MacroPluginLongId(Arc::new(RemoveOrigPlugin)),
MacroPluginLongId(Arc::new(DummyPlugin)),
]));
res
}
}
cairo_lang_test_utils::test_file_test!(
defs,
"src/test_data",
{
generic_item_id: "generic_item_id",
},
test_generic_item_id
);
fn test_generic_item_id(
inputs: &OrderedHashMap<String, String>,
_args: &OrderedHashMap<String, String>,
) -> TestRunnerResult {
let mut db_val: DatabaseForTesting = DatabaseForTesting::default();
setup_test_module(&mut db_val, inputs["module_code"].as_str());
let module_id = ModuleId::CrateRoot(get_crate_id(&db_val));
let file_id = db_val.module_main_file(module_id).unwrap();
let file_syntax = db_val.file_module_syntax(file_id).unwrap();
let mut output = String::new();
fn find_generics<'db>(
db: &'db dyn Database,
mut module_id: ModuleId<'db>,
node: &SyntaxNode<'db>,
output: &mut String,
) {
match node.kind(db) {
SyntaxKind::ItemModule => {
let submodule_id =
SubmoduleLongId(module_id, ast::ItemModulePtr(node.stable_ptr(db))).intern(db);
module_id = ModuleId::Submodule(submodule_id);
}
SyntaxKind::GenericParamType
| SyntaxKind::GenericParamConst
| SyntaxKind::GenericParamImplNamed
| SyntaxKind::GenericParamImplAnonymous => {
let param_id =
GenericParamLongId(module_id, ast::GenericParamPtr(node.stable_ptr(db)))
.intern(db);
let generic_item = param_id.generic_item(db);
writeln!(output, "{:?} -> {:?}", param_id.debug(db), generic_item.debug(db))
.unwrap();
}
_ => {}
}
for child in node.get_children(db).iter() {
find_generics(db, module_id, child, output);
}
}
find_generics(&db_val, module_id, &file_syntax.as_syntax_node(), &mut output);
TestRunnerResult::success(OrderedHashMap::from([("output".into(), output)]))
}
pub fn get_crate_id<'db>(db: &'db dyn Database) -> CrateId<'db> {
CrateId::plain(db, SmolStrId::from(db, "test"))
}
pub fn setup_test_module(db: &mut dyn Database, content: &str) {
let crate_id = get_crate_id(db);
let directory = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(directory)));
let crate_id = get_crate_id(db);
let file = db.module_main_file(ModuleId::CrateRoot(crate_id)).unwrap();
override_file_content!(db, file, Some(content.into()));
let crate_id = get_crate_id(db);
let file = db.module_main_file(ModuleId::CrateRoot(crate_id)).unwrap();
let syntax_diagnostics = db.file_syntax_diagnostics(file).format(db);
assert_eq!(syntax_diagnostics, "");
}
#[test]
fn test_module_file() {
let mut db_val = DatabaseForTesting::default();
setup_test_module(
&mut db_val,
indoc! {"
mod mysubmodule;
"},
);
let module_id = ModuleId::CrateRoot(get_crate_id(&db_val));
let db = &db_val;
let item_id = extract_matches!(
module_id.module_data(db).ok().unwrap().items(db)[0],
ModuleItemId::Submodule
);
assert_eq!(item_id.name(db).long(db), "mysubmodule");
let submodule_id = ModuleId::Submodule(item_id);
assert_eq!(
db.module_main_file(module_id).unwrap().long(db),
&FileLongId::OnDisk("src/lib.cairo".into())
);
assert_eq!(
db.module_main_file(submodule_id).unwrap().long(db),
&FileLongId::OnDisk("src/mysubmodule.cairo".into())
);
}
macro_rules! set_file_content {
($db:expr, $path:expr, $content:expr) => {
let file_id = FileLongId::OnDisk($path.into()).intern($db);
override_file_content!($db, file_id, Some($content.into()));
};
}
#[test]
fn test_submodules() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "mod submod;");
set_file_content!(db, "src/submod.cairo", "mod subsubmod;");
set_file_content!(db, "src/submod/subsubmod.cairo", "fn foo() {}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let submodule_id =
ModuleId::Submodule(*db.module_submodules_ids(module_id).unwrap().first().unwrap());
let subsubmodule_id =
ModuleId::Submodule(*db.module_submodules_ids(submodule_id).unwrap().first().unwrap());
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", subsubmodule_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[FreeFunctionId(test::submod::subsubmod::foo), ExternTypeId(test::submod::subsubmod::B)]"
);
assert_eq!(db.file_modules(db.module_main_file(module_id).unwrap()).unwrap(), &vec![module_id]);
assert_eq!(
db.file_modules(db.module_main_file(submodule_id).unwrap()).unwrap(),
&vec![submodule_id]
);
assert_eq!(
db.file_modules(db.module_main_file(subsubmodule_id).unwrap()).unwrap(),
&vec![subsubmodule_id]
);
}
#[derive(Debug)]
struct DummyPlugin;
impl MacroPlugin for DummyPlugin {
fn generate_code<'db>(
&self,
db: &'db dyn Database,
item_ast: ast::ModuleItem<'db>,
_metadata: &MacroPluginMetadata<'_>,
) -> PluginResult<'db> {
match item_ast {
ast::ModuleItem::Struct(struct_ast) => {
let remove_original_item = struct_ast.has_attr(db, "remove_original");
PluginResult {
code: Some(PluginGeneratedFile {
name: "virt".into(),
content: format!("fn f(x:{}){{}}", struct_ast.name(db).text(db).long(db)),
code_mappings: Default::default(),
aux_data: None,
diagnostics_note: Default::default(),
is_unhygienic: false,
}),
diagnostics: vec![],
remove_original_item,
}
}
ast::ModuleItem::FreeFunction(free_function_ast) => PluginResult {
code: Some(PluginGeneratedFile {
name: "virt2".into(),
content: "extern type B;".into(),
code_mappings: Default::default(),
aux_data: None,
diagnostics_note: Default::default(),
is_unhygienic: false,
}),
diagnostics: vec![PluginDiagnostic::error(
free_function_ast.stable_ptr(db),
"bla".into(),
)],
remove_original_item: false,
},
_ => PluginResult::default(),
}
}
fn declared_attributes<'db>(&self, db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
vec![SmolStrId::from(db, "remove_original")]
}
}
#[test]
fn test_plugin() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "struct A{}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[StructId(test::A), FreeFunctionId(test::f), ExternTypeId(test::B)]"
);
}
#[test]
fn test_plugin_remove_original() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "#[remove_original] struct A{}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[FreeFunctionId(test::f), ExternTypeId(test::B)]"
);
}
#[derive(Debug)]
struct RemoveOrigPlugin;
impl MacroPlugin for RemoveOrigPlugin {
fn generate_code<'db>(
&self,
db: &'db dyn Database,
item_ast: ast::ModuleItem<'db>,
_metadata: &MacroPluginMetadata<'_>,
) -> PluginResult<'db> {
let Some(free_function_ast) = try_extract_matches!(item_ast, ast::ModuleItem::FreeFunction)
else {
return PluginResult::default();
};
if !free_function_ast.has_attr(db, "remove_orig") {
return PluginResult::default();
}
PluginResult { code: None, diagnostics: vec![], remove_original_item: true }
}
fn declared_attributes<'db>(&self, db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
vec![SmolStrId::from(db, "remove_orig")]
}
}
#[derive(Debug)]
struct FooToBarPlugin;
impl MacroPlugin for FooToBarPlugin {
fn generate_code<'db>(
&self,
db: &'db dyn Database,
item_ast: ast::ModuleItem<'db>,
_metadata: &MacroPluginMetadata<'_>,
) -> PluginResult<'db> {
let Some(free_function_ast) = try_extract_matches!(item_ast, ast::ModuleItem::FreeFunction)
else {
return PluginResult::default();
};
if free_function_ast.declaration(db).name(db).text(db).long(db) != "foo" {
return PluginResult::default();
}
if !free_function_ast.has_attr(db, "foo_to_bar") {
return PluginResult::default();
}
PluginResult {
code: Some(PluginGeneratedFile {
name: "virt".into(),
content: "fn bar() {}".to_string(),
code_mappings: vec![],
aux_data: None,
diagnostics_note: Default::default(),
is_unhygienic: false,
}),
diagnostics: vec![],
remove_original_item: false,
}
}
fn declared_attributes<'db>(&self, db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
vec![SmolStrId::from(db, "foo_to_bar")]
}
}
#[test]
fn test_foo_to_bar() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "#[foo_to_bar] fn foo() {}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[FreeFunctionId(test::foo), FreeFunctionId(test::bar), ExternTypeId(test::B), \
ExternTypeId(test::B)]"
);
}
#[test]
fn test_first_plugin_removes() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "#[remove_orig] fn foo() {}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)), "[]");
}
#[test]
fn test_first_plugin_generates() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "#[foo_to_bar] #[remove_orig] fn foo() {}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[FreeFunctionId(test::bar), ExternTypeId(test::B)]"
);
}
#[test]
fn test_plugin_chain() {
let mut db_val = DatabaseForTesting::default();
let db = &mut db_val;
let crate_id = get_crate_id(db);
let root = Directory::Real("src".into());
set_crate_config!(db, crate_id, Some(CrateConfiguration::default_for_root(root)));
set_file_content!(db, "src/lib.cairo", "#[foo_to_bar] fn foo() {}");
let crate_id = get_crate_id(db);
let module_id = ModuleId::CrateRoot(crate_id);
let db_ref: &dyn Database = &*db;
assert_eq!(
format!("{:?}", module_id.module_data(db).unwrap().items(db).debug(db_ref)),
"[FreeFunctionId(test::foo), FreeFunctionId(test::bar), ExternTypeId(test::B), \
ExternTypeId(test::B)]"
)
}