We're going to have your attention.
We'd like to begin with the next talk.
We have Guillaume.
He's going to explain to us how to write your own Rust Linter, as you can see on the lovely
slides.
And if you talk, Luca, have we got the audio as unmuted and everything?
Perfect.
Wonderful.
Okay.
Take it away.
Hi, everyone.
I will try to speak loud so everyone can hear.
So like he mentioned today, I will explain to you how to write your own Rust Linter.
So first little presentation.
I'm Guillaume Goumez.
If you come every year, I give a talk, so now you should more or less remember me, I
think.
I'm a member of a few teams of Rust projects and I'm an engineer at Huawei.
So first, let's explain what a Linter is in case some people don't know yet what it
is.
A Linter is a tool that generally is an addition to a compiler of a language.
And here in Rust, I suppose everyone heard about Clippy.
At least I hope so.
The goal is to detect some very basic logic errors to suggest improvements for any method
you might use, anything you could use better.
The goal is to make your code better in short.
So now how is a Rust Linter actually working?
We are directly entering into the subject.
So let's say it's an extension of the Rust compiler.
The Rust compiler has an API, very unstable.
So very frequently we have to update the Linter to be able to still work with the Rust compiler.
And that's exactly how Clippy works.
So when Clippy is running, it's actually running a lot of parts of the compiler to get parts
like AST for people who don't know what AST is, it's a token representing your code.
So if you have the struct keyword, it's a keyword and it's a struct.
So that allows you to have information higher than.
But it's not only that because if you only had the AST information, you could only make
suggestions like, yeah, you use this generics but not in a good way so you can do it like
that, et cetera.
So the goal is to go beyond that and to get access to more information like a borrower
checker and everything.
So if you have a trait you're using but you could use another trait which does the same
thing but shorter, we can now suggest it because we have this information from the compiler.
But because of that, we have to update the Linter often or never update which version
of the compiler we are using.
So why does it need to be a rest compiler extension?
It's quite simple to explain.
So unless you want to reimplement all the parsing, the borrower checking and pretty much everything,
well, better use what is already existing and ask them likely to make their API public
so you can use them.
And that's exactly how things went with Clippy and that's exactly how I went as well.
So I mentioned a few limitations already.
So it can only work on crates compiled with the same Rusty version.
You don't see it with Clippy because it's tied with your compiler.
When you un-style a Clippy, it's tied with your current compiler version.
So it just works but it's something to keep in mind you will see later.
Like I mentioned, the Rusty API is not stable so very often you have to update your Linter
code to be able to keep up.
It's tied to a specific Rusty version and I'm not talking about a stable release but
literally to commit version which is a bit annoying.
And also because of everything, it's annoying to wrap in a cargo command because you need
to use a very specific Rusty version.
Again, we'll come back to that later.
So I will voluntarily don't mention all lint passes.
I will only speak of the two main ones, the early and the late passes.
The early passes give you access to AST.
So you are able to see the syntax and the work a bit on it but you don't have type information
or everything.
You can only just know that this is a struct and its name is and it has generics but you
don't know what traits it implements or anything.
You just have very basic information and you have the late pass which in this case goes
a lot further.
You have access to Borrowchecker information.
You have access to everything.
What does this type is implementing?
Does it implement this trait?
What is its layout?
Everything.
So in this case, we will talk about how to write a linter but with Rusty tools.
The goal of this trait is to wrap the Rusty API into something easier to set up because
there is a lot of things to set up.
And to add it, it's just that.
Like you would add any other trait.
For now, the version 0.3 later on it will be updated.
And now we start to enter into the fun.
So actually to make it work, you need to add this little line in your Kaggle file to tell
okay it's a trait but not any trait.
It's a Rusty trait.
So you need to do some very funny things.
And we'll come back to this one but it's some things that you thought were something that
we had for years like having to write an extant trait to import a trait.
As back you actually need to import a trait from the compiler with extant trait.
Otherwise it doesn't work.
It's not provided by default.
The other thing is we need to create a Rust toolchain file.
It's literally its name.
If you never use it, if you have a Rust toolchain file in your folder, cargo will only use the
version provided inside this file.
So in this case, the version of the compiler we're using.
This is all in the documentation of Rusty tools.
Basically you just need to copy and paste the file into your local file.
So in here we say that as components we want the Rusty div which means the traits from
the compiler.
We want Rust FMT because we are not savages.
We want to actually format our code.
And the LLVM tool, the preview is to be able to actually compile.
Otherwise you don't have a backend which is also problematic.
And now let's get into the code.
To declare a lint it's mostly macro.
As you can see on top we use internal Rusty traits.
So lint and session.
Lint provides some types linked to handling lints.
And session allows us to give information to the Rust compiler about things we want it
to run.
So in here we create with the declare tool lint macro a lint called warn generics.
In MAG.
In capital letters.
I can't do that.
It's warn by default.
And we add a message when it will, in case you want information about it, it says warns
if any item has generics.
It's an early lint pass.
So it means we only have access to the AST information.
I voluntarily picked this one because to be honest the code is much, much shorter and
simpler and for 15 minute talks it will be better.
The other thing we need to do is to implement some very basic traits provided by the compiler
which we don't need to care about.
So they provide a macro for that.
So declare lint pass which is in our case allowing you to declare a structure called
warn generics.
And we link it to the warn generics lint.
And after that at the end we have the very empty implementation of the early lint pass
trait for our type.
This visitor trait, if some don't know the visitor, how to say, pattern, visitor pattern,
let's say.
The visitor pattern allows you to have access to, literally, you implement whatever you
need to have and then, for example, visit function.
Whenever the visitor will encounter a function, it will call this method and it will be ours.
If we don't care about the rest, they are already implemented.
We don't need to care about them.
So very convenient.
In our case, we only want items that could have generics, so very likely functions and
n-us and everything like that.
So it will be pretty easy normally.
So now we implement the lint.
So as I was saying, check item.
We don't have anything else to do.
It provides a context, the context of the compiler at this stage, an early context, and
we have the actual item.
And then it's pretty simple.
We have methods provided by the compiler and everything.
So we check if our, I hope everyone knows the syntax of first, but we check that we
have generics.
We check that with some generics.
We check that the generics are not empty because otherwise there is no point.
If we have generics and everything, then we will say, okay, we found generics.
We don't want generics because, because, and let's then emit our lint.
So first, the lint name.
Second, the span.
The span is how the rest compiler map your size to your actual source code.
It's basically to your size beginning and an end.
And you don't have to care about what it's pointing to.
You just say, okay, the type I want to lint about is starting here, ending here.
You underline you do whatever you do and I don't care.
And we have our message saying, no generics here because we don't want generics.
And the last thing is in case you wanted to add more information, like for example, we
could say, the help and we could add a help message and we can do that a lot more.
In case some of you don't know what it is, the syntax with the straight bar is a closure.
So a closure taking a diagnostic type argument.
Now, the interesting part is now how can we run this lint?
And as you can see, not much code because RustyTools is doing pretty much everything.
So first, we get the cargo args because it's a cargo command.
We will run the cargo tools.
We don't want the two first arguments because cargo and tools are not something that we
are interested into.
We pass the rest of the arguments, if any, into the RustyTools cargo integration command,
which we internally call cargo, build everything with its own version because it's not necessarily
the case.
And once everything is built, it will generate the command line that you actually need to
pass the Rusty compiler to be able to run our linter, which we do with WistLint.
So this time, args is what cargo provided us so we can now generate and run our lint.
So we just give it access because it's already done by RustyTools.
And inside this WistLint, we need to actually say to the compiler, OK, I created a lint.
It's called not an void call.
I did badly.
It's a one generics.
And that's it.
We have everything.
We can now live.
And the compiler will do everything when living as a WistLint function.
So now it's always nicer to be able to run a cargo tool.
So you just run a cargo install dash dash pass if it's local, otherwise not.
And I named it in this case tools inner.
You will understand why later.
So we just run it.
And it doesn't work because we are not using the same version of the compiler.
Congrats.
So in this case, what's important to note is that you actually very much need to use
the same version of metadata as the files generated by the compiler to be able to use
them with the lint.
Rusty doesn't understand itself if it's not exactly the same.
Like if it's just a commit difference, no, I don't know him.
Don't care.
No problem.
So now we can actually go around this limitation by providing the version like this.
So if we do, I thought I had an error output.
So if we do, we actually have the tools running.
But to be fair, we can't really ask our user to do that themselves.
It's pretty bad user experience.
So we will go around that and do this very long file as you can see, which for this case
will be called the cargo tools.
And this one will literally run this command that we saw here itself.
And that's it.
It does just that.
We just wrap our linter and it's just running.
So now we install it.
We run it.
And again, I don't have output.
It's very shaming.
And believe me, it works.
So yeah.
I voluntarily, like I said, didn't show a late linter pass to have access to the compiler
and everything.
But I wrote a blog post explaining that much more in depth.
Inside it, you have an example with an unwrap if I remember, saying, yeah, don't use unwraps
use something else.
And you see how we actually get the real type information because when you call unwrap,
you need to check that unwrap is actually called on the result on an option.
But for that, you need to actually get the type check information because if it's, for
example, self with a capital letter double colon unwrap and then you pass your type,
you actually need to infer the type.
And for that, you need type check information.
You will see a lot of things that are seen very easy but are quite not so easy.
For example, if you want to have, I don't know, which type this implementation is being
implemented on, funnily enough, it's quite difficult.
You can have the trade very easily but the type it's being implemented on, not so much.
And thank you for your attention.
More information on my blog and you have my email and social media and everything.
And thank you for your attention.
So we have about two minutes for questions if anyone has them.
Yes, come right to the back.
Hello, thanks for this presentation.
No, don't share at all.
Okay.
Hello, again, thanks for this presentation.
A few years ago, I wrote a refinement type system for Rust as a linter.
I had the courage to maintain it for about one or two versions of Rust.
A few months ago, I tried to pick it up again and everything was broken,
bit rotten, two tiers, everything had changed.
Do you know if there are any plans to make things a bit less messy?
Because right now it's really, really, really painful to maintain a linter.
No, it's just pain, enjoy.
It's a shame.
No, in fact, it's actually better now because we have less function to worry about.
For example, a lot of APIs that were existing before, only for Rust.
Because Rust.doc is a compiler extension.
Being less and less used because we said, okay, we now stop accepting completely broken code.
And soon enough, we'll be very likely using the same API as Lins.
So normally it should be still breaking as much, but not as much.
I don't know.
How is this related to Clippy?
I don't hear you at all.
Ah.
Basically, it's working the same way, but it exists because in Clippy,
not all Lins can be implemented if you have specific needs for your project
because you need to have higher security levels or you don't want certain code pieces or everything.
You can't expect them to be implemented in Clippy.
So you implemented them yourself and that's very much why RustyTools exists.
So you can actually do it without having to set up everything yourself.
Perfect. Thank you so much.