Proposal: stabilizable, proc-macro based lints

Can confirm: there are a number of things that we would really like some lints for.

However, as Bevy is something of a DSL unto itself, they would pretty consistently be completely pointless (or even actively counterproductive) in other code bases. We also change the lints much faster to cope with the rapid pace of development, rather than having to rope in the clippy team.

This is a big one, isn't it? The entire reason why we have a stable proc-macro ecosystem is that proc-macros are based on "token trees" as an extremely general fairly unstructured API (compared to a proper AST) that avoids committing to almost anything.

OTOH, giving access to type information sounds like providing a lot more structure than even the parsed AST would have. I can't quite see how that could reuse any of the existing proc-macro infrastructure, and designing this API to be future-proof also sounds non-trivial.

So, I think the idea of custom lints is super exciting, but if you need type information (which you probably do) then you cannot really make it "proc-macro based", you have to build a whole new API for this. I think.

4 Likes

OK, I'm going to ask the following question out of pure ignorance: is it possible to have a crate built on top of something like syn that provides this information? Or is it absolutely impossible given some other set of behavior/guarantees/etc.? The one limitation I can see immediately is if you need to know about the types of objects that will only be known after the macro is evaluated, e.g., types literally defined after the macro is used within some file, or objects that are defined within a different crate altogether.

The other possibility is to have an API that is forever unstable. That is, it is stabilized (hah!) as an unstable-only feature, which may change at any time, and tool authors have to accept when they write lints. That would mean that rust could continue to grow and shift as needed. Part of that guarantee is that the type information might be incomplete at the point that the macro is called; that is, if you reorder your code, you might get more type information in your macro, and that's just normal and expected behavior.

I don't think so. To get type information you need access to context -- other modules in this crate, and other crates.

There's a lot happening between the token tree and HIR (on which most lints run) -- name resolution and trait resolution, to just name two big ones. (And macro expansion of course, as you mentioned.)

1 Like

That won't work. How will crater run results have to be interpreted if some low-level API breaks an entire dep tree? I'm afraid that anything unstable has to live behind a nightly feature flag and not be available from a stable compiler. The alternative is just as if Rust didn't have its stability guarantees because going from "it will work (modulo soundness fixes)" to "it will work (unless you have custom lints, maybe)" is a huge gap that I don't think I'd like to see Rust try to cross.

Yeah, that's indeed massive.

Two interesting languages which did pull the trick of exposing type information for meta-programming purposes without (to the best of my knowledge) painting the compiler into backwrads-compat corner, are Dart and C#:

Interestingly, the approach there is very different from what Rust does with procedural macros.

I may have used the wrong terminology, because what you're saying is exactly what I was suggesting. The tongue-in-cheek 'stabilization' is that it is known to forever be unstable and in nightly...

I guess if you're fine with your lints only being available in nightly compilers, but I don't think that's reasonable for something that is called "stable". There are a few things which are "perma-unstable", but I believe they are the building blocks for other things which are stable. I don't think a "perma-unstable" feature for its own sake sounds great (especially given how much API work is likely involved).

I don't know if proc macros are exactly the right tool for this, but I do think that we need a general mechanism for crate-provided lints, including rustfix-applicable suggestions.

I think a good path forward for this might be to propose a lang team initiative, starting from a problem statement of allowing crates to provide lints to run on their dependencies. That initiative could look at possible solutions for this, with proc macros as a leading candidate, though we'd need to evaluate how precisely to pull in the proc macro (because ideally you shouldn't have to add an #[attribute] to various functions just to get a lint warning, but we also shouldn't have multiple proc macros running over huge amounts of potentially unrelated code).

5 Likes

I know I'm kind of waving hands here, but some of this has been discussed by myself and a couple others on Zulip recently. I have a high-level plan for user-provided lints in my head. There's already buy-in from a couple users (in addition to myself): @flip1995 @xFrednet and I think @shepmaster. I'm hoping to get some time tomorrow to write things down and have it out on IRLO on Wednesday.

Like I said, I know its a bit hand-wavey, but a day or two of patience will hopefully yield quite a bit of clarity on how this is possible and giving a plausible path forward with far-reaching consequences.

7 Likes

That sounds promising. I'd love to see that plan, and then we can talk about the right next steps to support this in the language.

just for future reference: here's a link to @jhpratt's post

2 Likes

Right after the section you quote, I do show in my proposal how I think proc-macro lints could ask for type information. I could definitely implement my sqlx_lints::uncommitted_transaction lint with that. Sure, it might get tripped up in more complex cases but we're just talking a simple lint to catch a typical beginner's mistake. It's not the end of the world if it doesn't catch everything.

I can also easily conceive of a way to query the compiler for a TokenStream of the definition of a type/trait/impl/function for more complex lints.

Is it perfect? No, I'll gladly admit that. The important thing is that there is a very clear path to stabilization vs developing a whole new API for interfacing with the compiler. This proposal can act as a stopgap solution or a fallback, because any hypothetical API effort can easily fall to excessive bikeshedding, or conflicting requirements, or the feature shepherd just plain losing interest or not having time to finish it. Stabilization with that large of an API surface is also going to be a whole ordeal in itself.

My proposal I will gladly start implementing right now, and I figure I could have an MVP in maybe a month. I almost didn't even bother with writing a proposal first but I figured the PR would get rejected without an RFC. It seems the main benefit of posting this proposal wasn't to actually workshop the feature anyway, but to learn about concurrent efforts and existing workarounds, so I guess it's already served its purpose.

IIRC proc macros themselves were purely meant as a stopgap. They're certainly flawed and limited. However, their replacement has yet to materialize, and I certainly think we're better off having them than if we had decided not to implement them and to wait for a more optimal solution. It's the same thing here. I don't want to be sitting here three years down the road still thinking, "man, I really wish I could implement my own lints."

I show in my proposal how the proc-macro lint could tell the compiler to only run it for certain things. Also, since lints are idempotent they can easily be run in parallel which would help reduce the perceived overhead they add.

This is not stored for definitions from other crates currently.

That's not as much of an issue because if you're writing lints for your own crate, or just any one specific crate really, then you don't really need to query the definitions of things since you can just hardcode that knowledge in the lint. Sure, that's more coupled than many people might like but that can be improved later.

If you're looking for the definition of an arbitrary item anywhere in the dep graph, then that's starting to get out of the scope of the kind of lints I'm interested in. At that point you're likely talking about a lint that makes sense to live in Clippy since you're looking at more generic patterns and not just linting for usage of a specific crate.