Skip to content

Conversation

jDomantas
Copy link
Contributor

  • Renamed existing generate_enum_match_method to generate_enum_is_variant
  • Added two similar assists to generate into_ and as_ methods.
  • Made all of them general enough to work on record and tuple variants too.

For as_ method generation there's room to improve:

  • Right now it always returns Option<&Field>, even though Option<Field> would be nicer when Field: Copy. I don't know how to check if the field type implements Copy. If given suggestions I could try to fix this in a follow-up pr.
  • &String could be replaced with &str, &Box<_> with &_, and probably some more. I don't know what would be a good way to do that.

Closes #7604

@matklad
Copy link
Contributor

matklad commented Feb 14, 2021

bors d=@yoshuawuyts

@bors
Copy link
Contributor

bors bot commented Feb 14, 2021

✌️ yoshuawuyts can now approve this pull request. To approve and merge a pull request, simply reply with bors r+. More detailed instructions are available here.

Copy link
Member

@yoshuawuyts yoshuawuyts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR @jDomantas! -- I think this is heading in a really good direction. To summarize some of the feedback from the PR:

  1. The generated methods should be parameterized by the output type, rather than the variant name. So as_str rather than as_text.
  2. The code generated by the into_ assist should pass clippy and adhere to the Rust naming conventions.
  3. The shared code would likely go well into assists/utils.rs
  4. The newly added lints should go into their own files. Especially if the first two points are addressed they'll be quite a bit different from the generate_enum_is_method assist.

I hope this feedback's helpful; thanks so much for this PR, and feel free to ping once you're ready for another review.

Comment on lines 235 to 250
enum VariantKind {
Unit,
/// Tuple with a single field
NewtypeTuple {
ty: Option<ast::Type>,
},
/// Tuple with 0 or more than 2 fields
Tuple,
/// Record with a single field
NewtypeRecord {
field_name: Option<ast::Name>,
field_type: Option<ast::Type>,
},
/// Record with 0 or more than 2 fields
Record,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the kind of functionality for which I'd expect an abstraction to already exist within Rust-Analyzer. cc/ @Veykril

If it turns out this is something new, at least for now it'd still be good to have this live under assists/utils.rs so other assists can also make use of this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no abstraction like this yet to my knowledge, most assists only required ast::StructKind which doesn't discern between single field record/tuples.

Copy link
Member

@Veykril Veykril Feb 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from the looks of it, it doesn't really make sense to keep the Option in the variants here. The binding_pattern and single_field_type functions just propogate the None values which are then propogated at every call site as well from what I can tell. I think making the variant_kind constructor return an Option<VariantKind> instead when any of those are None makes more sense, with that the inherent functions on VariantKind then can just return things directly.

Comment on lines 213 to 304
fn add_method_to_adt(
builder: &mut AssistBuilder,
adt: &ast::Adt,
impl_def: Option<ast::Impl>,
method: &str,
) {
let mut buf = String::with_capacity(method.len() + 2);
if impl_def.is_some() {
buf.push('\n');
}
buf.push_str(method);

let start_offset = impl_def
.and_then(|impl_def| find_impl_block_end(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&adt, &buf);
adt.syntax().text_range().end()
});

builder.insert(start_offset, buf);
}

enum VariantKind {
Unit,
/// Tuple with a single field
NewtypeTuple {
ty: Option<ast::Type>,
},
/// Tuple with 0 or more than 2 fields
Tuple,
/// Record with a single field
NewtypeRecord {
field_name: Option<ast::Name>,
field_type: Option<ast::Type>,
},
/// Record with 0 or more than 2 fields
Record,
}

impl VariantKind {
fn pattern_suffix(&self) -> &'static str {
match self {
VariantKind::Unit => "",
VariantKind::NewtypeTuple { .. } | VariantKind::Tuple => "(..)",
VariantKind::NewtypeRecord { .. } | VariantKind::Record => " { .. }",
}
}

fn binding_pattern(&self) -> Option<(String, String)> {
match self {
VariantKind::Unit
| VariantKind::Tuple
| VariantKind::Record
| VariantKind::NewtypeRecord { field_name: None, .. } => None,
VariantKind::NewtypeTuple { .. } => Some(("(v)".to_owned(), "v".to_owned())),
VariantKind::NewtypeRecord { field_name: Some(name), .. } => {
Some((format!(" {{ {} }}", name.syntax()), name.syntax().to_string()))
}
}
}

fn single_field_type(&self) -> Option<&ast::Type> {
match self {
VariantKind::Unit | VariantKind::Tuple | VariantKind::Record => None,
VariantKind::NewtypeTuple { ty } => ty.as_ref(),
VariantKind::NewtypeRecord { field_type, .. } => field_type.as_ref(),
}
}
}

fn variant_kind(variant: &ast::Variant) -> VariantKind {
match variant.kind() {
ast::StructKind::Record(record) => {
if let Some((single_field,)) = record.fields().collect_tuple() {
let field_name = single_field.name();
let field_type = single_field.ty();
VariantKind::NewtypeRecord { field_name, field_type }
} else {
VariantKind::Record
}
}
ast::StructKind::Tuple(tuple) => {
if let Some((single_field,)) = tuple.fields().collect_tuple() {
let ty = single_field.ty();
VariantKind::NewtypeTuple { ty }
} else {
VariantKind::Tuple
}
}
ast::StructKind::Unit => VariantKind::Unit,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these to assists/utils.rs instead?

Comment on lines 80 to 100
// enum Value {
// Number(i32),
// Text(String)$0,
// }
// ```
// ->
// ```
// enum Value {
// Number(i32),
// Text(String),
// }
//
// impl Value {
// fn into_text(self) -> Option<String> {
// if let Self::Text(v) = self {
// Some(v)
// } else {
// None
// }
// }
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name

I would've expected the generated method to be parameterized by the name of the output type, rather than the name of the enum variant. as is always followed by the type we're converting into (e.g. slice::as_ptr or ThreadId::as_u64).

Suggested change
// enum Value {
// Number(i32),
// Text(String)$0,
// }
// ```
// ->
// ```
// enum Value {
// Number(i32),
// Text(String),
// }
//
// impl Value {
// fn into_text(self) -> Option<String> {
// if let Self::Text(v) = self {
// Some(v)
// } else {
// None
// }
// }
// }
// enum Value {
// Number(i32),
// Text(String)$0,
// }
// ```
// ->
// ```
// enum Value {
// Number(i32),
// Text(String),
// }
//
// impl Value {
// fn into_string(self) -> Option<String> {
// if let Self::Text(v) = self {
// Some(v)
// } else {
// None
// }
// }
// }

Output type

Running clippy on the generated function produces the following warning:

warning: methods called `into_*` usually take self by value; consider choosing a less ambiguous name
  --> src/lib.rs:15:20
   |
15 |     fn into_string(&self) -> Option<&String> {
   |                    ^^^^^
   |
   = note: `#[warn(clippy::wrong_self_convention)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#wrong_self_convention

This matches the Rust naming guidelines and means we should probably only ever generate this function if we know we will never fail the conversion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you get that? It does generate into_* methods with self as the parameter, and as_* methods with &self.

Copy link
Contributor Author

@jDomantas jDomantas Feb 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with your point about the function names. Naming the function by the type is good as long as the function is about the type conversion. A key difference is that these functions are intended to extract the payload from a specific enum variant, so they are precisely about the variant instead of the type. And when the function is generated for a specific variant it is weird to name them by type when some other variants have the same payload type, or when the payload is a generic type parameter. Also I think that naming them by the type would also be inconsistent with is_ functions which are very similar in purpose and are precise about variant.

The concrete example I had in mind when I saw the request for these was rowan's NodeOrToken methods. Another example is Result::ok and Result::err - they also use the name to indicate which variant is extracted, but don't use the as_ and into_ naming convention. (although I guess that the fact that both of these examples are generic enums does not really help my case as referring by type would be super weird)

So maybe the naming convention for these could be different - I don't know if there's actually a convention for enum payload extracting functions (by the way, the request in #7604 did ask for as_variant_name()). But in any case I think that the function needs to name the variant name, and not the payload type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(although I guess that the fact that both of these examples are generic enums does not really help my case as referring by type would be super weird)

Maybe we can extrapolate a rule which is consistent with both cases: if the type we're referencing is a generic value we should use the variant name. If it's not, we should reference the output type. I agree that NodeOrToken::as_n() and Result::t() wouldn't be very good names. However I also think that Value::as_str which returns &str is clearer than Value::as_text.


It does generate into_* methods with self as the parameter

Ah apologies, that's my bad. I probably wrongly copied / edited in playground; we can indeed disregard my comments on clippy 😅 .

However I'm still somewhat uncomfortable with auto-generating into functions which return Option. This seems like a fallible conversion for which try_into would be a better name. But instead of returning Option, Result may be a better fit.

The Result::Err path here is important because it allow recovering the original type through the error path in case the conversion fails. An example of this is TryFrom<Vec<T, A>> for [T; N] which has Vec<T, A> as the error type. In the Rowan example, if the conversion fails the original type is gone. The Copy bound helps work around that; but Rust-Analyzer shouldn't assume all structs to have Copy bounds, which makes this hard to generalize.

I'd prefer either into_x conversions which always succeed, or try_into_x conversions which return the original type in case they fail (or both?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still disagree with naming by type. I looked for more examples, and I found a couple (who would have guessed that non-error enums would be so difficult to find):

  • serde_json::Value. This is a fun one. First of, it has as_object, as_array, and even as_null. These make much more sense than as_map, as_vec, or as_unit because they speak in json terminology, so when reading the conversions it is a lot more obvious about what json kind is expected. Naming by type could have also lead to some very non-useful names - as_map could have also ended up being as_btree_map or as_index_map, which is just silly for manipulating json values. Now the interesting part is that this enum also has as_i64, as_u64, and as_f64. These are clearly about type conversion - there is no variant which would be named like that or would have a payload of corresponding type. Instead these check both for Number variant, and also that the payload is actually representable as the requested type.
  • I also grepped rust-lang/rust for similar pattern, and the only real example I found was SocketAddr::as_pathname. Return type is Path, but the name says "pathname" because it actually checks if it a pathname address.

I think serde_json example also shows that conversion to a type can have a more complex meaning than just matching for a single variant and that meaning would not be appropriate for an assist to guess. Also, remember the case when multiple variants have the same payload type. It would be weird for a function named as_str to be only about one of the variants holding a string. How would you even name the function for the other variant?

I agree with your point about try_into_x though, being able to recover the original value is useful. I changed the assist to instead generate try_into_x returning Result<Payload, Self>.

// }
//
// impl Value {
// fn as_text(&self) -> Option<&String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with the into_ assist; the function here should be parameterized by the output type rather than the enum variant name.

)
}

// Assist: generate_enum_into_method
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assists seem different enough from the existing one that it'd probably be a good idea to put them in their own files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, that indeed does look a lot nicer. The VariantKind enum was a bit contrived to be useful for both is_ assist and the new ones, but now it's not needed anymore and just got replaced with a single match in each method!

};

// Assist: generate_enum_match_method
// Assist: generate_enum_is_method
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're renaming the method we should rename the file probably too.

@jDomantas
Copy link
Contributor Author

Hey, my last comment was not replied to in a week. Do I still need to change it to generate the name based on type? I did not find the argument in favor of that convincing and personally I wouldn't have a use for this assist then.

@yoshuawuyts

@yoshuawuyts
Copy link
Member

yoshuawuyts commented Feb 27, 2021

@jDomantas It seems you've taken my feedback into serious consideration, and made a clear enough argument that there's a case for either variant. Though this variant doesn't have my preference, I think the best step forward from here is to merge this and get feedback from users.

bors r+

@bors
Copy link
Contributor

bors bot commented Feb 27, 2021

@bors bors bot merged commit 4a24edd into rust-lang:master Feb 27, 2021
@jDomantas jDomantas deleted the more-enum-matching branch February 27, 2021 20:59
@lnicola
Copy link
Member

lnicola commented Mar 1, 2021

as_try_into

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Assist proposal: generate a as_variant() method for enum
5 participants