Following on from my introduction to clippy lints, this week I am beginning my journey of actually implementing a clippy lint.
As a refresher, I am implementing rust-lang/rust-clippy#1673:
To summarize, if the type has a
get_foomethod we should suggest naming itfooinstead to follow the API Guidelines for Rust getter name conventions except for cases of:
getget_mutget_uncheckedget_unchecked_mutget_refThis should be enough to get me started on a style lint for this convention, I should have some time over the next couple days to start digging into this.
clippy’s Author Lint
In typical TDD fashion, I want to start with the test case, the code I want to lint against, so I can test my implementation and drive design. I’ve started off with the simple case of detecting the invalid style rather than worrying about whitelisting the exceptions identified.
pub struct MyStruct {
id: u32
}
impl MyStruct {
pub fn get_id(&self) -> u32 {
self.id
}
}
fn main() {
let s = MyStruct { id: 42 };
#[clippy::author]
let id = s.get_id();
}
I’ve also added the #[clippy::author] annotation as suggested by clippy’s contributing documentation to generate a starting point for the lint.
Next, I have to run the test to produce a .stdout file with the code generated by the #[clippy::author] lint. The instructions say:
If the command was executed successfully, you can copy the code over to where you are implementing your lint.
Let’s try it out:
$ TESTNAME=ui/getter_prefix cargo test --test compile-test
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running target/debug/deps/compile_test-f89d0316ceade355
running 1 test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out
running 1 test
test [ui] ui/getter_prefix.rs ... FAILED
failures:
---- [ui] ui/getter_prefix.rs stdout ----
normalized stdout:
if_chain! {
if let StmtKind::Decl(ref decl, _) = stmt.node
if let DeclKind::Local(ref local) = decl.node;
if let Some(ref init) = local.init
if let ExprKind::MethodCall(ref method_name, ref generics, ref args) = init.node;
// unimplemented: `ExprKind::MethodCall` is not further destructured at the moment
if let PatKind::Binding(BindingAnnotation::Unannotated, _, name, None) = local.pat.node;
if name.node.as_str() == "id";
then {
// report your lint here
}
}
expected stdout:
diff of stdout:
+if_chain! {
+ if let StmtKind::Decl(ref decl, _) = stmt.node
+ if let DeclKind::Local(ref local) = decl.node;
+ if let Some(ref init) = local.init
+ if let ExprKind::MethodCall(ref method_name, ref generics, ref args) = init.node;
+ // unimplemented: `ExprKind::MethodCall` is not further destructured at the moment
+ if let PatKind::Binding(BindingAnnotation::Unannotated, _, name, None) = local.pat.node;
+ if name.node.as_str() == "id";
+ then {
+ // report your lint here
+ }
+}
+
The actual stdout differed from the expected stdout.
Actual stdout saved to /Users/scrust/devel/rust-clippy/target/debug/test_build_base/getter_prefix.stdout
To update references, run this command from build directory:
tests/ui/update-references.sh '/Users/scrust/devel/rust-clippy/target/debug/test_build_base' 'getter_prefix.rs'
error: 1 errors occurred comparing output.
status: exit code: 0
command: "target/debug/clippy-driver" "tests/ui/getter_prefix.rs" "-L" "/Users/scrust/devel/rust-clippy/target/debug/test_build_base" "--target=x86_64-apple-darwin" "-C" "prefer-dynamic" "-o" "/Users/scrust/devel/rust-clippy/target/debug/test_build_base/getter_prefix.stage-id" "-L" "target/debug" "-L" "target/debug/deps" "-Dwarnings" "-L" "/Users/scrust/devel/rust-clippy/target/debug/test_build_base/getter_prefix.stage-id.aux" "-A" "unused"
stdout:
------------------------------------------
if_chain! {
if let StmtKind::Decl(ref decl, _) = stmt.node
if let DeclKind::Local(ref local) = decl.node;
if let Some(ref init) = local.init
if let ExprKind::MethodCall(ref method_name, ref generics, ref args) = init.node;
// unimplemented: `ExprKind::MethodCall` is not further destructured at the moment
if let PatKind::Binding(BindingAnnotation::Unannotated, _, name, None) = local.pat.node;
if name.node.as_str() == "id";
then {
// report your lint here
}
}
------------------------------------------
stderr:
------------------------------------------
------------------------------------------
thread '[ui] ui/getter_prefix.rs' panicked at 'explicit panic', /Users/scrust/.cargo/registry/src/github.com-1ecc6299db9ec823/compiletest_rs-0.3.17/src/runtest.rs:2553:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
[ui] ui/getter_prefix.rs
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 225 filtered out
test compile_test ... FAILED
failures:
---- compile_test stdout ----
thread 'compile_test' panicked at 'Some tests failed', /Users/scrust/.cargo/registry/src/github.com-1ecc6299db9ec823/compiletest_rs-0.3.17/src/lib.rs:89:22
failures:
compile_test
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--test compile-test'
Wow that’s a lot out output. More importantly, I don’t actually know if it worked. I see
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out
but then I see
The actual stdout differed from the expected stdout.
Actual stdout saved to /Users/scrust/devel/rust-clippy/target/debug/test_build_base/getter_prefix.stdout
and
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 225 filtered out
test compile_test ... FAILED
I also don’t see any generated .stdout file, so I’m going to assume there’s something wrong with my test case.
If I remove the author lint tag, I get a different result:
$ TESTNAME=ui/getter_prefix cargo test --test compile-test
Finished dev [unoptimized + debuginfo] target(s) in 0.16s
Running target/debug/deps/compile_test-f89d0316ceade355
running 1 test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out
running 1 test
test [ui] ui/getter_prefix.rs ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 225 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
test compile_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
This all seems to pass, but I still don’t see a generated .stdout file, and I’ve also lost any output the #[clippy:author] annotation would have given me.
I noticed a line from the earlier, possibly failed, test run:
To update references, run this command from build directory: tests/ui/update-references.sh ‘/Users/scrust/devel/rust-clippy/target/debug/test_build_base’ ‘getter_prefix.rs’
What does this do?
What’s a “reference”?
clippy’s documentation doesn’t seem to mention tests/ui/update-references.sh at all, though there is a mention of tests/ui/update-all-references.sh which seems to update all of the existing .stderr files that drive the UI tests.
It turns out that running tests/ui/update-references.sh is necessary to actually write the .stdout file.
I wasn’t expecting this extra step because the way the instructions are phrased I though running the test would generate the .stdout file automatically.
For the test I wrote, #[clippy::author] generated a .stdout file with the following code:
if_chain! {
if let StmtKind::Decl(ref decl, _) = stmt.node
if let DeclKind::Local(ref local) = decl.node;
if let Some(ref init) = local.init
if let ExprKind::MethodCall(ref method_name, ref generics, ref args) = init.node;
// unimplemented: `ExprKind::MethodCall` is not further destructured at the moment
if let PatKind::Binding(BindingAnnotation::Unannotated, _, name, None) = local.pat.node;
if name.node.as_str() == "id";
then {
// report your lint here
}
}
I’m not at all familiar with the if_chain! macro or any of these datatypes, so I definitely have some reading to do so I can understand what this snippet actually does.
I do see if name.node.as_str() == "id"; which seems to match the id field on MyStruct which is about the only piece I understand without delving deeper.
Out of curiosity after reading a number of other lint tests, I decided to update main and added an assert statement:
fn main() {
let s = MyStruct { id: 42 };
#[clippy::author]
let id = s.get_id();
assert_eq!(id, 42);
}
and got yet another different result:
$ TESTNAME=ui/getter_prefix cargo test --test compile-test
Finished dev [unoptimized + debuginfo] target(s) in 0.16s
Running target/debug/deps/compile_test-f89d0316ceade355
running 1 test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out
running 1 test
test [ui] ui/getter_prefix.rs ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 225 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
test compile_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
I’m not sure the #[clippy::author] annotation is going to help me too much in implementing this lint.
Right now I don’t know enough about clippy or the datatypes it uses to make heads or tails of the generated code, and the results of my test are inconsistent depending on various combinations of assert_eq! and the author annotation.
This definitely calls for more research, so until next week it looks like I’ll be getting to grips with some of rustc’s compiler internals.