rust-lang / rust-analyzer

A Rust compiler front-end for IDEs
https://rust-analyzer.github.io/
Apache License 2.0
14.33k stars 1.61k forks source link

False positive error when using bilge #17873

Open showier-drastic opened 3 months ago

showier-drastic commented 3 months ago

rust-analyzer version: rust-analyzer version: 0.3.2070-standalone (0daeb5c0b 2024-08-10) [/Users/user/.vscode/extensions/rust-lang.rust-analyzer-0.3.2070-darwin-arm64/server/rust-analyzer]

rustc version: rustc 1.80.1 (3f5fd8dd4 2024-08-06)

editor or extension: VSCode v0.3.2070

relevant settings: No special settings

repository link (if public, optional): https://github.com/showier-drastic/bilge-test

I'm using bilge. This library provides the #[bitsize(3)] macro. The code in the repo above compiles successfully without errors, but rust analyzer complains about it, every time #[bitsize] is used:

add #[bitsize] attribute above your derive attribute rust-analyzer[macro-error](https://rust-analyzer.github.io/manual.html#macro-error)

Upstream issue: https://github.com/hecatia-elegua/bilge/issues/92

code snippet to reproduce:

#[bitsize(14)]
#[derive(FromBits)]
struct Register {
    header: u4,
    body: u7,
    footer: Footer,
}
ChayimFriedman2 commented 2 months ago

First of all, this macro does something exceedingly cursed, which IMO we should officially not support, because those who use this pattern deserve a punishment.

Jokes aside, the macro reuse the same attribute for both proc macro attribute and derive helper attribute. Somehow, rustc seams to bless this pattern. I bet this wasn't done knowingly, and I'm not sure whether the language team would be happy to commit to this, but rustc allows that so we are obliged :(

alexkl-rugged-controls commented 1 day ago

I'm not entirely sure of this, but it seems what is really happening is that bilge just happens to insert a macro before some derive macros, and rust-analyzer has a bug where it repeats derive macros once for every non-derive macro evaluation:

#[proc_macro_attribute]
pub fn test_attr(_: TokenStream, b: TokenStream) -> TokenStream {
    println!("Test Attr");
    b 
}

#[proc_macro_derive(TestDerive)]
pub fn test_derive(_: TokenStream) -> TokenStream {
    println!("Test Derive");
    TokenStream::new()
}

#[proc_macro_derive(TestDerive2)]
pub fn test_derive_2(_: TokenStream) -> TokenStream {
    println!("Test Derive 2");
    TokenStream::new()
}

#[derive(TestDerive, TestDerive2)]
#[test_attr]
#[test_attr]
struct _Foo {
    f: u8,
}
$ cargo test
   Compiling test_proc_macros v0.1.0 (/home/alexkl/src/test_proc_macros)
Test Derive
Test Derive 2
Test Attr
Test Attr
$ rust-analyzer diagnostics .
2024-11-22T23:04:15.004046Z ERROR proc-macro tried to print : Test Derive
2024-11-22T23:04:15.004331Z ERROR proc-macro tried to print : Test Derive 2
2024-11-22T23:04:15.004582Z ERROR proc-macro tried to print : Test Attr
2024-11-22T23:04:15.004942Z ERROR proc-macro tried to print : Test Derive
2024-11-22T23:04:15.005137Z ERROR proc-macro tried to print : Test Derive 2
2024-11-22T23:04:15.005355Z ERROR proc-macro tried to print : Test Attr
2024-11-22T23:04:15.005586Z ERROR proc-macro tried to print : Test Derive
2024-11-22T23:04:15.005753Z ERROR proc-macro tried to print : Test Derive 2

This seems pretty terrible for performance as it evaluates the macros num_attributes * num_derive_attributes times.

After each evaluation of an attribute, the attribute is stripped, and the set of derives is evaluated once more. This is why the bilge crate has the "false positive error", because what it wants to do is this:

// Original Code
#[bitsize(2)]
#[derive(DebugBits, Copy)]
struct Example {}

// Step 1
#[derive(DebugBits)] // While this is evaluated, it has access to `bitsize_internal(2)`
#[bitsize_internal(2)]
#[derive(Copy)]
struct Example {}

// Step 2
#[bitsize_internal(2)]
#[derive(Copy)]
struct Example {}
impl Debug for Example {/* Makes use of bitsize 2 */}

// Step 3
#[derive(Copy)] // Doesn't need bitsize 2, and should be implemented on the final struct.
struct Example { /* Rewritten fields */ }
impl Debug for Example {/* Makes use of bitsize 2 */}

// Done
struct Example { /* Rewritten fields */ }
impl Debug for Example {/* Makes use of bitsize 2 */}
impl Copy  for Example { }

But, with rust-analyzer what really happens is that when #[bitsize_internal] is being evaluated in Step 2->3, #[derive(DebugBits)] happens again, this time without access to #[bitsize(2)].