Potential rustc lint bug

Consider this code:

enum Foo {
    bar, // lower-cased variant 
}

fn main() {
    // NOP
}

This code rightly triggers a lint warning that Foo::bar should be camel-cased.
However, when I change the code to

enum Foo {
    #[allow(non_camel_case_types)] bar,
}

fn main() {
    // NOP
}

or

#[allow(non_camel_case_types)] 
enum Foo {
    bar,
}

fn main() {
    // NOP
}

What I expected to happen was that the warning would be silenced.
Instead, this code still triggers the same warning.

Is this a bug, or is this intended behavior?
(Though for the life of me I can't imagine why this would be an intended behavior. Perhaps it's an unintended side-effect of otherwise intended behavior?)

For completeness' sake, this does succeed in making rustc omit the error:

#![allow(non_camel_case_types)] 

enum Foo {
    bar,
}

fn main() {
    // NOP
}

I can't reproduce this problem. Playground.

Hmm I can see that what you're saying is true. And yet the situation described still persists in my real code.

The real enum is a bit more complex, as it has a derive attribute, a tuple struct variant and a bunch of unit struct variants. So now I'm guessing something about that cocktail is confusing the compiler, because what else could it possibly be?

I would suspect the derive in particular. You could try cargo-expand to see how that finally looks.

1 Like

From first principles I wonder how the derive could even influence that. Derive macro's are limited to generating new code, and have read-only access to the type's tokens, correct? So what could possibly be generated that generates a warning of this kind on the input enum variant itself?

For reference, these are the traits being derived: Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize and one I've written called Delta that appropriately enough calculates deltas for 2 values of the same type as well as apply those deltas. But like I said, above, AFAIK derives have read-only access to their input, so it would have to be some generated code somehow that is triggering the warning on the input enum.

Also a note on cargo-expand: I actually tried to use it during the development of my derive macro. My takeaway was that its usability is greatly hampered by the fact that it does an expand-all in lisp terms, while what I need is an expand-1 i.e. just 1 level of macro expansion. This is because I don't care about generated code that uses macros in the output. In fact its presence is detrimental because it means that the information I care about (i.e. the signal) gets buried between bits and pieces of information I don't care about (i.e. the noise).

A derive could be creating a type with a name based on your type, and the warning is emitted due to the macro-created-type. I don't think Serialize and Deserialize do this, none of the std derives do this, so maybe Delta?

2 Likes

The Delta derive macro does in fact do this.
Okay so given that I also have the fix (which is easier said than done though): copy over the tokens for the enum variant's attribute macro to the corresponding enum variant of the generated type.

You could unconditionally #[allow(nonstandard_style)] on your derived code. If the user has something weird, they'll get those warnings on their own part, or mask it themselves, but I don't think you should worry about that on your part.

1 Like

That's a pretty excellent solution that I didn't know existed. Thanks for the tip, that will make it a lot easier to fix this, at least for the short term.

The long term solution will still require passing of attribute macro tokens from input to generated output, because attributes other than this one may be applied to enum variants.

This isn't correct. In fact, it's quite common for derives to delete stuff that's there; this is how custom attributes to control the derive usually work.

You can see this from the signature:

pub fn hello_macro_derive(input: TokenStream) -> TokenStream {

This takes ownership of the TokenStream, and returns an owned one. Nothing prevents you from doing whatever you want, other than it likely confusing users and causing them to not use your derive.

No, derives don't replace their input. The original type that the derive macro was applied to stays intact.

https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros

The input TokenStream is the token stream of the item that has the derive attribute on it. The output TokenStream must be a set of items that are then appended to the module or block that the item from the input TokenStream is in.

(emphasis mine)

only attribute/function like macros replace their inputs

3 Likes

Hmmm, thank you! I didn't think this was true, but tested it, and you're right! That said, I'm confused as to how serde implements renaming:

use serde::*;
use serde_derive::*;

#[derive(Serialize)]
struct Foo {
    #[serde(rename = "name")]
    x: i32,
}

fn main() {
}

My understanding was that the #[serde] bits were removed from the tokenstream.

cargo expand shows that it leaves them in, then. I'm now very confused as to how this works!

I have defined a #[deltoid(ignore_field)] attribute as part of the attribute macro that, when applied to a field, ignores that field when computing and applying a delta. (so no, luckily it's not removed from the token stream). Basically it's just a part of the input which can be parsed using syn, at which point you have an enum and struct based AST. So it's possible to just scan the AST for the information required, and generate code accordingly.

1 Like

serde just passes in the new field name to serialize_field instead of extracting to actual field name. If serde changed the actual type, you couldn't use the names you declared in your code.

https://docs.serde.rs/serde/ser/trait.SerializeStruct.html#tymethod.serialize_field

1 Like

Derive macros can declare a number of “helper” attributes that they will parse during the expansion. These helpers are then treated as noop attribute macros after the expansion has completed. One key for this is that Serialize and Deserialize share the same attributes, if one of them were to remove the attribute during expansion then the second to run would not get to see it.

3 Likes

Rustc knows that these attributes belong to the derive macros, and ignores them for all other purposes. When declaring a derive proc macro, the macro programmer also declares all the attributes it takes in, like:

#[proc_macro_derive(Serialize, attributes(serde))]
fn ...

As I understand it, then, the attributes declared by a proc macro are treated as valid, and ignored for all purposes except being sent to proc macros. I think they're also scoped per-derive: a derive macro won't see #[serde(...)] unless it declares the serde attribute.

1 Like

You can always display what your macro is outputting yourself:

and then, if needed, you can pipe that to something like rustfmt

That is roughly what I do now, albeit with a feature flag provided to the derive macro. In fact there are 2, one that dumps to stdout and one that dumps each type's derive expansion to its own file.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.