Skip to content

Commit 74282e2

Browse files
authored
Defining, parsing, and checking #[attribute]s (#6986)
## Description This PR reimplements how `#[attribute]`s are parsed, defined, and checked. It fixes the following issues we had with attributes: - attributes were in general not checked for their expected and allowed usage. - when checked, checks were fixing particular reported issues and were scattered through various compilation phases: lexing, parsing, and tree conversion. - when checked, reported errors in some cases had bugs and were overalapping with other errors and warnings. - attribute handling was not centralized, but rahter scattered throught the whole code base, and mostly done at use sites. For concrete examples of these issues, see the list of closed issues below. The PR: - encapsulates the attributes handling within a single abstraction: `Attributes`. - assigns the following properties to every attribute: - _target_: what kind of elements an attribute can annotate. - _multiplicity_: if more then one attribute of the same kind can be applied to an element. - _arguments multiplicity_: a range defining how many arguments an attribute can or must have. - _arguments value expectation_: if attribute arguments are expected to have values. - validates those properties in a single place, during the `convert_parse_tree`. Note that this means that modules with attribute issues will now consistently not reach the type checking phase. This was previously the case for some of the issues with attributes where custom checks where done during the type checking phase. #6987 proposes to move those checks after the tree conversion phase, allowing the continuation of compilation. - adds the `AttributeKind::Unknow` to preserve unknown attributes in the typed tree. - removes redundant code related to attributes: specific warnings and errors, specialized parsing, repetitive and error-prone at use site checks, etc. - removes the unused `Doc` attribute. The PR also introduces a new pattern in emitting errors. The `attr_decls_to_attributes` function will always return `Attributes` even if they have errors, because: - we expect the compilation to continue even in the case of errors. - we expect those errors to be ignored if a valid `#[cfg]` attribute among those evaluates to false. - we expect them to be emitted with eventual other errors, or alone, at the end of the tree conversion of the annotated element. For more details, see the comment on `attr_decls_to_attributes` and `cfg_eval`. Closes #6880; closes #6913; closes #6914; closes #6915; closes #6916; closes #6917; closes #6918; closes #6931; closes #6981; closes #6983; closes #6984; closes #6985. ## Breaking changes Strictly seen, this PR can cause breaking changes, but only in the case of invalid existing attribute usage. We treat those breaking changes as bug fixes in the compiler and, thus, do not have them behind a feature flag. E.g., this kind of code was possible before, but will now emit and error: ```sway #[storage(read, write, read, write)] struct Struct {} ``` ## Checklist - [x] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers.
1 parent cdcbd2f commit 74282e2

File tree

316 files changed

+8738
-2017
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

316 files changed

+8738
-2017
lines changed

.typos.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ extend-ignore-identifiers-re = [
1313
extend-exclude = [
1414
"docs/book/theme/highlight.js",
1515
"docs/reference/theme/highlight.js",
16-
"forc-plugins/forc-doc/src/static.files/highlight.js"
16+
"forc-plugins/forc-doc/src/static.files/highlight.js",
17+
"test/src/e2e_vm_tests/test_programs/should_fail/attributes_invalid_args/**",
18+
"test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_unknown/**",
1719
]

docs/book/src/blockchain-development/calling_contracts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ When a contract is compiled, a special section called "contract selection" is al
171171
1 - if no fallback function was specified, the contract will revert;
172172
2 - otherwise, the fallback function will be called.
173173
174-
For all intents and purposes the fallback function is considered a contract method, which means that it has all the limitations that other contract methods have. As the fallback function signature, the function cannot have arguments, but they can return anything.
174+
For all intents and purposes the fallback function is considered a contract method, which means that it has all the limitations that other contract methods have. As for the fallback function signature, the function cannot have arguments, but it can return anything.
175175
176176
If for some reason the fallback function needs to returns different types, the intrinsic `__contract_ret` can be used.
177177
@@ -182,7 +182,7 @@ abi MyContract {
182182
fn some_method();
183183
}
184184
185-
impl ContractB for Contract {
185+
impl MyContract for Contract {
186186
fn some_method() {
187187
}
188188
}
@@ -192,8 +192,8 @@ fn fallback() {
192192
}
193193
```
194194
195-
You may still access the method selector and arguments to the call in the fallback.
196-
For instance, let's assume a function `fn foobar(bool, u64) {}` gets called on a contract that doesn't have it with arguments `true` and `42`.
195+
You may still access the method selector and call arguments in the fallback function.
196+
For instance, let's assume a function `fn foobar(bool, u64) {}` gets called on a contract that doesn't have it, with arguments `true` and `42`.
197197
It can execute the following fallback:
198198
199199
```sway

docs/reference/src/code/language/annotations/src/main.sw

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ fn bar() {}
9090

9191

9292
// ANCHOR: allow_deprecated_annotation
93-
#[deprecated(note = "this is deprecated")]
93+
#[deprecated(note = "This is deprecated.")]
9494
struct DeprecatedStruct {}
9595

9696
#[allow(deprecated)]

forc-pkg/src/pkg.rs

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::{
2929
sync::{atomic::AtomicBool, Arc},
3030
};
3131
use sway_core::namespace::Package;
32+
use sway_core::transform::AttributeArg;
3233
pub use sway_core::Programs;
3334
use sway_core::{
3435
abi_generation::{
@@ -44,7 +45,6 @@ use sway_core::{
4445
language::parsed::TreeType,
4546
semantic_analysis::namespace,
4647
source_map::SourceMap,
47-
transform::AttributeKind,
4848
write_dwarf, BuildTarget, Engines, FinalizedEntry, LspConfig,
4949
};
5050
use sway_core::{set_bytecode_configurables_offset, PrintAsm, PrintIr};
@@ -2003,49 +2003,55 @@ impl PkgEntryKind {
20032003

20042004
impl PkgTestEntry {
20052005
fn from_decl(decl_ref: &DeclRefFunction, engines: &Engines) -> Result<Self> {
2006+
fn get_invalid_revert_code_error_msg(
2007+
test_function_name: &Ident,
2008+
should_revert_arg: &AttributeArg,
2009+
) -> String {
2010+
format!("Invalid revert code for test \"{}\".\nA revert code must be a string containing a \"u64\", e.g.: \"42\".\nThe invalid revert code was: {}.",
2011+
test_function_name,
2012+
should_revert_arg.value.as_ref().expect("`get_string_opt` returned either a value or an error, which means that the invalid value must exist").span().as_str(),
2013+
)
2014+
}
2015+
20062016
let span = decl_ref.span();
20072017
let test_function_decl = engines.de().get_function(decl_ref);
20082018

2009-
const FAILING_TEST_KEYWORD: &str = "should_revert";
2019+
let Some(test_attr) = test_function_decl.attributes.test() else {
2020+
unreachable!("`test_function_decl` is guaranteed to be a test function and it must have a `#[test]` attribute");
2021+
};
20102022

2011-
let test_args: HashMap<String, Option<String>> = test_function_decl
2012-
.attributes
2013-
.get(&AttributeKind::Test)
2014-
.expect("test declaration is missing test attribute")
2023+
let pass_condition = match test_attr
2024+
.args
20152025
.iter()
2016-
.flat_map(|attr| attr.args.iter())
2017-
.map(|arg| {
2018-
(
2019-
arg.name.to_string(),
2020-
arg.value
2021-
.as_ref()
2022-
.map(|val| val.span().as_str().to_string()),
2023-
)
2024-
})
2025-
.collect();
2026+
// Last "should_revert" argument wins ;-)
2027+
.rfind(|arg| arg.is_test_should_revert())
2028+
{
2029+
Some(should_revert_arg) => {
2030+
match should_revert_arg.get_string_opt(&Handler::default()) {
2031+
Ok(should_revert_arg_value) => TestPassCondition::ShouldRevert(
2032+
should_revert_arg_value
2033+
.map(|val| val.parse::<u64>())
2034+
.transpose()
2035+
.map_err(|_| {
2036+
anyhow!(get_invalid_revert_code_error_msg(
2037+
&test_function_decl.name,
2038+
should_revert_arg
2039+
))
2040+
})?,
2041+
),
2042+
Err(_) => bail!(get_invalid_revert_code_error_msg(
2043+
&test_function_decl.name,
2044+
should_revert_arg
2045+
)),
2046+
}
2047+
}
2048+
None => TestPassCondition::ShouldNotRevert,
2049+
};
20262050

2027-
let pass_condition = if test_args.is_empty() {
2028-
anyhow::Ok(TestPassCondition::ShouldNotRevert)
2029-
} else if let Some(args) = test_args.get(FAILING_TEST_KEYWORD) {
2030-
let expected_revert_code = args
2031-
.as_ref()
2032-
.map(|arg| {
2033-
let arg_str = arg.replace('"', "");
2034-
arg_str.parse::<u64>()
2035-
})
2036-
.transpose()?;
2037-
anyhow::Ok(TestPassCondition::ShouldRevert(expected_revert_code))
2038-
} else {
2039-
let test_name = &test_function_decl.name;
2040-
bail!("Invalid test argument(s) for test: {test_name}.")
2041-
}?;
2042-
2043-
let file_path = Arc::new(
2044-
engines.se().get_path(
2045-
span.source_id()
2046-
.ok_or_else(|| anyhow::anyhow!("Missing span for test function"))?,
2047-
),
2048-
);
2051+
let file_path =
2052+
Arc::new(engines.se().get_path(span.source_id().ok_or_else(|| {
2053+
anyhow!("Missing span for test \"{}\".", test_function_decl.name)
2054+
})?));
20492055
Ok(Self {
20502056
pass_condition,
20512057
span,

forc-plugins/forc-doc/src/render/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use std::{
1919
collections::BTreeMap,
2020
ops::{Deref, DerefMut},
2121
};
22-
use sway_core::{language::ty::TyProgramKind, transform::AttributesMap};
22+
use sway_core::{language::ty::TyProgramKind, transform::Attributes};
2323
use sway_types::BaseIdent;
2424

2525
pub mod constant;
@@ -71,7 +71,7 @@ impl RenderedDocumentation {
7171
pub fn from_raw_docs(
7272
raw_docs: Documentation,
7373
render_plan: RenderPlan,
74-
root_attributes: Option<AttributesMap>,
74+
root_attributes: Option<Attributes>,
7575
program_kind: &TyProgramKind,
7676
forc_version: Option<String>,
7777
) -> Result<RenderedDocumentation> {

forc-plugins/forc-doc/src/render/util/format/docstring.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
use crate::render::util::format::constant::*;
33
use comrak::{markdown_to_html, ComrakOptions};
44
use std::fmt::Write;
5-
use sway_core::transform::{AttributeKind, AttributesMap};
5+
use sway_core::transform::{AttributeKind, Attributes};
66
use sway_lsp::utils::markdown::format_docs;
77

88
pub(crate) trait DocStrings {
99
fn to_html_string(&self) -> String;
1010
fn to_raw_string(&self) -> String;
1111
}
12-
/// Creates an HTML String from an [AttributesMap]
13-
impl DocStrings for AttributesMap {
12+
/// Creates an HTML String from [Attributes].
13+
impl DocStrings for Attributes {
1414
fn to_html_string(&self) -> String {
1515
let docs = self.to_raw_string();
1616

@@ -26,15 +26,14 @@ impl DocStrings for AttributesMap {
2626
markdown_to_html(&format_docs(&docs), &options)
2727
}
2828
fn to_raw_string(&self) -> String {
29-
let attributes = self.get(&AttributeKind::DocComment);
3029
let mut docs = String::new();
31-
32-
if let Some(vec_attrs) = attributes {
33-
for arg in vec_attrs.iter().flat_map(|attribute| &attribute.args) {
34-
writeln!(docs, "{}", arg.name.as_str()).expect(
35-
"problem appending `arg.name.as_str()` to `docs` with `writeln` macro.",
36-
);
37-
}
30+
// TODO: Change this logic once https://github.com/FuelLabs/sway/issues/6938 gets implemented.
31+
for arg in self
32+
.of_kind(AttributeKind::DocComment)
33+
.flat_map(|attribute| &attribute.args)
34+
{
35+
writeln!(docs, "{}", arg.name.as_str())
36+
.expect("problem appending `arg.name.as_str()` to `docs` with `writeln` macro.");
3837
}
3938
docs
4039
}

forc-plugins/forc-migrate/src/cli/commands/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ fn print_migration_finished_action(num_of_postponed_steps: usize) {
451451
"Finished",
452452
&format!(
453453
"Run `forc migrate` at a later point to resolve {} postponed migration step{}",
454-
number_to_str(num_of_postponed_steps),
454+
num_to_str(num_of_postponed_steps),
455455
plural_s(num_of_postponed_steps),
456456
),
457457
)

forc-plugins/forc-migrate/src/matching/lexed_tree.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,7 @@ pub mod __mod_name {
227227
self.items
228228
.__iter()
229229
.filter_map(|annotated| match __ref([annotated.value]) {
230-
ItemKind::Impl(item_impl) => {
231-
Some((__ref([annotated.attribute_list]), item_impl))
232-
}
233-
// ItemKind::Impl(__ref_mut([item_impl])) => Some((__ref([annotated.attribute_list]), item_impl)),
230+
ItemKind::Impl(item_impl) => Some((__ref([annotated.attributes]), item_impl)),
234231
_ => None,
235232
})
236233
.find(|(_attributes, item_impl)| item_impl.span() == ty_element.span)
@@ -255,7 +252,6 @@ pub mod __mod_name {
255252
attribute::{Attribute, AttributeArg},
256253
AttributeDecl, CommaToken, Parens, Punctuated,
257254
};
258-
use sway_types::constants::CFG_ATTRIBUTE_NAME;
259255

260256
pub(crate) fn storage_decl<'a, P>(parent: __ref_type([P])) -> Option<__ref_type([ItemStorage])>
261257
where
@@ -306,7 +302,7 @@ pub mod __mod_name {
306302
attributes
307303
.__iter()
308304
.flat_map(|attr| attr.attribute.inner.__iter())
309-
.filter(|attr| attr.name.as_str() == CFG_ATTRIBUTE_NAME)
305+
.filter(|attr| attr.is_cfg())
310306
}
311307

312308
/// Returns all `cfg` attributes that act as only attribute within
@@ -323,7 +319,7 @@ pub mod __mod_name {
323319
.__iter()
324320
.filter(|attr| attr.attribute.inner.iter().count() == 1)
325321
.flat_map(|attr| attr.attribute.inner.__iter())
326-
.filter(|attr| attr.name.as_str() == CFG_ATTRIBUTE_NAME)
322+
.filter(|attr| attr.is_cfg())
327323
.filter(|attr| {
328324
attr.args
329325
.as_ref()

forc-plugins/forc-migrate/src/migrations/partial_eq.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ fn remove_deprecated_eq_trait_implementations_interaction(
277277
for annotated_eq_impl in annotated_eq_impls {
278278
// Check if the `#[cfg(experimental_partial_eq = false)]` attribute exists.
279279
if lexed_match::cfg_attribute_standalone_single_arg(
280-
&annotated_eq_impl.attribute_list,
280+
&annotated_eq_impl.attributes,
281281
EXPERIMENTAL_PARTIAL_EQ_ATTRIBUTE,
282282
|arg| arg.as_ref().is_some_and(literal::is_bool_false),
283283
)
@@ -330,7 +330,7 @@ fn remove_deprecated_eq_trait_implementations_interaction(
330330
// Check if the `#[cfg(experimental_partial_eq = true)]` attribute exists.
331331
let Some(cfg_experimental_partial_eq_attr) =
332332
lexed_match::cfg_attribute_standalone_single_arg(
333-
&annotated_trait_impl.attribute_list,
333+
&annotated_trait_impl.attributes,
334334
EXPERIMENTAL_PARTIAL_EQ_ATTRIBUTE,
335335
|arg| arg.as_ref().is_some_and(literal::is_bool_true),
336336
)
@@ -386,7 +386,7 @@ fn find_trait_impl(
386386
let attributed_eq_trait_impls = lexed_match::impl_self_or_trait_decls_annotated(module)
387387
.filter_map(|annotated| match &annotated.value {
388388
ItemKind::Impl(item_impl) if item_impl::implements_trait(trait_name)(&item_impl) => {
389-
Some((&annotated.attribute_list, item_impl))
389+
Some((&annotated.attributes, item_impl))
390390
}
391391
_ => None,
392392
});
@@ -523,7 +523,7 @@ fn implement_experimental_partial_eq_and_eq_traits(
523523

524524
// Set the `experimental_partial_eq` attribute to true.
525525
let Some(experimental_partial_eq_arg) = lexed_match_mut::cfg_attribute_arg(
526-
&mut annotated_impl_partial_eq_trait.attribute_list,
526+
&mut annotated_impl_partial_eq_trait.attributes,
527527
with_name_mut(EXPERIMENTAL_PARTIAL_EQ_ATTRIBUTE),
528528
) else {
529529
bail!(internal_error(

forc-plugins/forc-migrate/src/modifying/annotated.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl<T> Modifier<'_, Annotated<T>> {
1818
&mut self,
1919
attribute_span: &Span,
2020
) -> &mut Self {
21-
self.element.attribute_list.retain(|attribute_decl| {
21+
self.element.attributes.retain(|attribute_decl| {
2222
attribute_decl
2323
.attribute
2424
.inner

forc-plugins/forc-migrate/src/modifying/attribute.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
use sway_ast::{
2-
attribute::{Attribute, AttributeArg},
2+
attribute::{Attribute, AttributeArg, CFG_ATTRIBUTE_NAME, DOC_COMMENT_ATTRIBUTE_NAME},
33
brackets::SquareBrackets,
44
keywords::{HashBangToken, HashToken, Token},
55
AttributeDecl, Literal, Parens, Punctuated,
66
};
7-
use sway_types::{
8-
constants::{CFG_ATTRIBUTE_NAME, DOC_COMMENT_ATTRIBUTE_NAME},
9-
Ident, Span, Spanned,
10-
};
7+
use sway_types::{Ident, Span, Spanned};
118

129
use crate::assert_insert_span;
1310

forc-plugins/forc-migrate/src/modifying/module.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl Modifier<'_, Module> {
5050

5151
pub(crate) fn append_function(&mut self, function: ItemFn) -> &mut Self {
5252
let function = Annotated {
53-
attribute_list: vec![],
53+
attributes: vec![],
5454
value: ItemKind::Fn(function),
5555
};
5656
self.append_annotated_item(function)

0 commit comments

Comments
 (0)