-
Notifications
You must be signed in to change notification settings - Fork 359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add macros package and the with_components macro #1282
base: main
Are you sure you want to change the base?
Add macros package and the with_components macro #1282
Conversation
…eat/add-macros-package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work on this @ericnordelo! I left some comments
Also, since so much of the component boilerplate is abstracted away, I don't think it's super obvious what the storage entry is for a given component. It'll be documented, for sure, but would it make sense just to simplify it further and use the same capitalized name as the whitelisted component i.e.
#[with_components(ERC20, Ownable)]
#[starknet::contract]
pub mod MyToken {
(...)
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.Ownable.initializer(owner);
self.ERC20.initializer("MyToken", "MTK");
}
}
This gives a point of reference inside the contract as opposed to attempting, for example, reentrancy_guard
or reentrancyguard
...ts/openzeppelin_macros__tests__test_with_components__with_access_control_no_initializer.snap
Outdated
Show resolved
Hide resolved
...snapshots/openzeppelin_macros__tests__test_with_components__with_account_no_initializer.snap
Show resolved
Hide resolved
.../openzeppelin_macros__tests__test_with_components__with_erc1155_receiver_no_initializer.snap
Show resolved
Hide resolved
"AccessControl" => Some(ComponentInfo { | ||
name: "AccessControlComponent".to_string(), | ||
path: "openzeppelin_access::accesscontrol::AccessControlComponent".to_string(), | ||
storage: "accesscontrol".to_string(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. I know the lib isn't entirely consistent (might be worth addressing in a separate issue), but I think it could get weird when we have timelock_controller
vs accesscontrol
and reentrancyguard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will refactor them here (for macros). We may update presets and mocks in a separate issue later.
Co-authored-by: Andrew Fleming <[email protected]>
Co-authored-by: Andrew Fleming <[email protected]>
Co-authored-by: Andrew Fleming <[email protected]>
Co-authored-by: Andrew Fleming <[email protected]>
Co-authored-by: Andrew Fleming <[email protected]>
Since developers are already using snake case for storage members, I vote we stay with snake case, but split the words accordingly. |
This is true...no strong pushback against keeping snake case, I just think reusing the name could be a bit more intuitive. If we wanted to make a change like this, it'd be better to do it now with the feature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done, Eric, great job!
I left several minor suggestions of how we could improve the code structure
if component_info.is_none() { | ||
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(diagnostics); | ||
} | ||
components_info.push(component_info.unwrap()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid unsafe unwrapping when possible:
if component_info.is_none() { | |
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(diagnostics); | |
} | |
components_info.push(component_info.unwrap()); | |
if let Some(info) = component_info { | |
components_info.push(info); | |
} else { | |
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(diagnostics); | |
} |
let parsed = db.parse_virtual(item_stream.to_string()); | ||
if parsed.is_err() { | ||
let error_message = parsed.err().unwrap().format(&db); | ||
let error = Diagnostic::error(error_message); | ||
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(error.into()); | ||
} | ||
|
||
// 3. Build the patch | ||
let node = parsed.unwrap(); | ||
let (content, mut diagnostics) = build_patch(&db, node, components_info.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let parsed = db.parse_virtual(item_stream.to_string()); | |
if parsed.is_err() { | |
let error_message = parsed.err().unwrap().format(&db); | |
let error = Diagnostic::error(error_message); | |
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(error.into()); | |
} | |
// 3. Build the patch | |
let node = parsed.unwrap(); | |
let (content, mut diagnostics) = build_patch(&db, node, components_info.clone()); | |
let (content, mut diagnostics) = match db.parse_virtual(item_stream.to_string()) { | |
Ok(node) => build_patch(&db, node, components_info.clone()), | |
Err(err) => { | |
let error = Diagnostic::error(err.format(&db)); | |
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(error.into()); | |
} | |
} |
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add an else clause with an error?
if let RewriteNode::Copied(copied) = node {
...
} else {
// panic with an error
}
let components_with_initializer_missing_str = | ||
components_with_initializer_missing.join(", "); | ||
if !components_with_initializer_missing.is_empty() { | ||
let warning = Diagnostic::warn(formatdoc! {" | ||
It looks like the initializers for the following components are missing: | ||
|
||
{components_with_initializer_missing_str} | ||
|
||
This may lead to unexpected behavior. We recommend adding the corresponding initializer calls to the constructor. | ||
"}); | ||
warnings.push(warning); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can move the line building the components_with_initializer_missing_str
into the if clause
let components_with_initializer_missing_str = | |
components_with_initializer_missing.join(", "); | |
if !components_with_initializer_missing.is_empty() { | |
let warning = Diagnostic::warn(formatdoc! {" | |
It looks like the initializers for the following components are missing: | |
{components_with_initializer_missing_str} | |
This may lead to unexpected behavior. We recommend adding the corresponding initializer calls to the constructor. | |
"}); | |
warnings.push(warning); | |
} | |
if !components_with_initializer_missing.is_empty() { | |
let components_with_initializer_missing_str = components_with_initializer_missing.join(", "); | |
let warning = Diagnostic::warn(formatdoc! {" | |
It looks like the initializers for the following components are missing: | |
{components_with_initializer_missing_str} | |
This may lead to unexpected behavior. We recommend adding the corresponding initializer calls to the constructor. | |
"}); | |
warnings.push(warning); | |
} |
let mut constructor_code = String::new(); | ||
let constructor = body.items_vec(db).into_iter().find(|item| { | ||
matches!(item, ast::ModuleItem::FreeFunction(function_ast) if function_ast.has_attr(db, CONSTRUCTOR_ATTRIBUTE)) | ||
}); | ||
if constructor.is_some() { | ||
// Get the constructor code (maybe we can do this without the builder) | ||
let constructor_ast = constructor.unwrap().as_syntax_node(); | ||
let typed = ast::ModuleItem::from_syntax_node(db, constructor_ast.clone()); | ||
let constructor_rnode = RewriteNode::from_ast(&typed); | ||
|
||
let mut builder = PatchBuilder::new_ex(db, &constructor_ast); | ||
builder.add_modified(constructor_rnode); | ||
(constructor_code, _) = builder.build(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid unsafe unwrapping and to make the constructor_code
var immutable:
let mut constructor_code = String::new(); | |
let constructor = body.items_vec(db).into_iter().find(|item| { | |
matches!(item, ast::ModuleItem::FreeFunction(function_ast) if function_ast.has_attr(db, CONSTRUCTOR_ATTRIBUTE)) | |
}); | |
if constructor.is_some() { | |
// Get the constructor code (maybe we can do this without the builder) | |
let constructor_ast = constructor.unwrap().as_syntax_node(); | |
let typed = ast::ModuleItem::from_syntax_node(db, constructor_ast.clone()); | |
let constructor_rnode = RewriteNode::from_ast(&typed); | |
let mut builder = PatchBuilder::new_ex(db, &constructor_ast); | |
builder.add_modified(constructor_rnode); | |
(constructor_code, _) = builder.build(); | |
} | |
let constructor = body.items_vec(db).into_iter().find(|item| { | |
matches!(item, ast::ModuleItem::FreeFunction(function_ast) if function_ast.has_attr(db, CONSTRUCTOR_ATTRIBUTE)) | |
}); | |
let constructor_code = if let Some(constructor) = constructor { | |
// Get the constructor code (maybe we can do this without the builder) | |
let constructor_ast = constructor.as_syntax_node(); | |
let typed = ast::ModuleItem::from_syntax_node(db, constructor_ast.clone()); | |
let constructor_rnode = RewriteNode::from_ast(&typed); | |
let mut builder = PatchBuilder::new_ex(db, &constructor_ast); | |
builder.add_modified(constructor_rnode); | |
(constructor_code, _) = builder.build(); | |
} else { | |
String::new() | |
}; |
let error = Diagnostic::error(formatdoc! {" | ||
Contract module must have the `#[{CONTRACT_ATTRIBUTE}]` attribute. | ||
"}); | ||
return (vec![error], vec![]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return (vec![error], vec![]); | |
return (vec![error], warnings); |
/// Allowed components are: | ||
/// `ERC20`, `Ownable` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should update the list of the allowed components:
/// Allowed components are: | |
/// `ERC20`, `Ownable` | |
/// Allowed components are: | |
/// `ERC20`, `Ownable` |
/// | ||
/// Allowed components are: | ||
/// `ERC20`, `Ownable` | ||
fn get_component_info(name: &str) -> (Option<ComponentInfo>, Diagnostics) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's better to change the function's output type to Result<ComponentInfo, Diagnostics>. It would make both the function itself and the code calling it more clear
fn get_component_info(name: &str) -> (Option<ComponentInfo>, Diagnostics) { | |
fn get_component_info(name: &str) -> Result<ComponentInfo, Diagnostics> { |
pub struct ComponentInfo { | ||
pub name: String, | ||
pub path: String, | ||
pub storage: String, | ||
pub event: String, | ||
pub has_initializer: bool, | ||
pub has_immutable_config: bool, | ||
pub internal_impls: Vec<String>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we don't need owned string in the code, so we could use &str
type instead of String
to make code cleaner and more performant. Especially the get_component_info
will benefit from it if we can get rid of all the to_string()
calls
pub struct ComponentInfo { | |
pub name: String, | |
pub path: String, | |
pub storage: String, | |
pub event: String, | |
pub has_initializer: bool, | |
pub has_immutable_config: bool, | |
pub internal_impls: Vec<String>, | |
} | |
pub struct ComponentInfo { | |
pub name: &str, | |
pub path: &str, | |
pub storage: &str, | |
pub event: &str, | |
pub has_initializer: bool, | |
pub has_immutable_config: bool, | |
pub internal_impls: Vec<&str>, | |
} |
internal_impls: vec!["InternalImpl".to_string()], | ||
}), | ||
_ => None, | ||
}; | ||
if info.is_none() { | ||
let allowed_components = ALLOWED_COMPONENTS.join(", "); | ||
let error_message = formatdoc! {" | ||
Invalid component: {name} | ||
|
||
Allowed components are: | ||
{allowed_components} | ||
"}; | ||
let error = Diagnostic::error(error_message); | ||
return (None, error.into()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal_impls: vec!["InternalImpl".to_string()], | |
}), | |
_ => None, | |
}; | |
if info.is_none() { | |
let allowed_components = ALLOWED_COMPONENTS.join(", "); | |
let error_message = formatdoc! {" | |
Invalid component: {name} | |
Allowed components are: | |
{allowed_components} | |
"}; | |
let error = Diagnostic::error(error_message); | |
return (None, error.into()); | |
} | |
internal_impls: vec!["InternalImpl".to_string()], | |
}), | |
_ => { | |
let allowed_components = ALLOWED_COMPONENTS.join(", "); | |
let error_message = formatdoc! {" | |
Invalid component: {name} | |
Allowed components are: | |
{allowed_components} | |
"}; | |
let error = Diagnostic::error(error_message); | |
Err(error.into()) | |
} | |
} |
Quickstart #1258
NOTE: A summary of the features included in the macro is added below after the example:
Example:
Simplifies this:
Into this:
Summary of features:
IT DOES:
use openzeppelin_access::ownable::OwnableComponent;
)component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
).impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
)ownable: OwnableComponent::Storage,
)IT DOES NOT:
Things to be considered still:
openzeppelin_access
vsopenzeppelin::access
in use clauses (configurable notation?)Some may be addressed in different PRs/issues.
NOTE: I have plans to include a
with_component
macro which will accept only a single component but more configuration, like replacing the storage entry. People then may use both macros together (with_components
andwith_component
)PR Checklist