From 30202a24021587b7d7ddffd8312eb5b425b3e273 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 17 May 2021 10:42:57 -0700 Subject: [PATCH] compilers/rust: Add support for clippy Clippy is a compiler wrapper for rust that provides an extra layer of linting. It's quite popular, but unfortunately doesn't provide the output of the compiler that it's wrapping in it's output, so we don't detect that clippy is rustc. This small patch adds a new compiler class (that is the Rustc class with a different id) and the necessary logic to detect that clippy is in fact rustc) Fixes: #8767 --- .../snippets/rust-clippy-driver-support.md | 6 +++++ mesonbuild/compilers/__init__.py | 3 ++- mesonbuild/compilers/detect.py | 23 ++++++++++++------- mesonbuild/compilers/rust.py | 17 ++++++++++++++ test cases/rust/1 basic/clippy.toml | 1 + test cases/rust/1 basic/prog.rs | 3 ++- unittests/allplatformstests.py | 13 +++++++++++ unittests/baseplatformtests.py | 5 ++-- 8 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 docs/markdown/snippets/rust-clippy-driver-support.md create mode 100644 test cases/rust/1 basic/clippy.toml diff --git a/docs/markdown/snippets/rust-clippy-driver-support.md b/docs/markdown/snippets/rust-clippy-driver-support.md new file mode 100644 index 000000000..c486473e7 --- /dev/null +++ b/docs/markdown/snippets/rust-clippy-driver-support.md @@ -0,0 +1,6 @@ +## Support for clippy-driver as a rustc wrapper + +Clippy is a popular linting tool for Rust, and is invoked in place of rustc as a +wrapper. Unfortunately it doesn't proxy rustc's output, so we need to have a +small wrapper around it so that Meson can correctly detect the underlying rustc, +but still display clippy diff --git a/mesonbuild/compilers/__init__.py b/mesonbuild/compilers/__init__.py index 8c5275c91..98b0b5f27 100644 --- a/mesonbuild/compilers/__init__.py +++ b/mesonbuild/compilers/__init__.py @@ -112,6 +112,7 @@ __all__ = [ 'PGICPPCompiler', 'PGIFortranCompiler', 'RustCompiler', + 'ClippyRustCompiler', 'CcrxCCompiler', 'CcrxCPPCompiler', 'Xc16CCompiler', @@ -241,7 +242,7 @@ from .objcpp import ( ClangObjCPPCompiler, GnuObjCPPCompiler, ) -from .rust import RustCompiler +from .rust import RustCompiler, ClippyRustCompiler from .swift import SwiftCompiler from .vala import ValaCompiler from .mixins.visualstudio import VisualStudioLikeCompiler diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py index 6426380a6..52ad7f3a1 100644 --- a/mesonbuild/compilers/detect.py +++ b/mesonbuild/compilers/detect.py @@ -124,7 +124,7 @@ from .objcpp import ( GnuObjCPPCompiler, ) from .cython import CythonCompiler -from .rust import RustCompiler +from .rust import RustCompiler, ClippyRustCompiler from .swift import SwiftCompiler from .vala import ValaCompiler from .mixins.visualstudio import VisualStudioLikeCompiler @@ -952,6 +952,13 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust continue version = search_version(out) + cls: T.Type[RustCompiler] = RustCompiler + + # Clippy is a wrapper around rustc, but it doesn't have rustc in it's + # output. We can otherwise treat it as rustc. + if 'clippy' in out: + out = 'rustc' + cls = ClippyRustCompiler if 'rustc' in out: # On Linux and mac rustc will invoke gcc (clang for mac @@ -976,7 +983,7 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust extra_args: T.Dict[str, T.Union[str, bool]] = {} always_args: T.List[str] = [] if is_link_exe: - compiler.extend(RustCompiler.use_linker_args(cc.linker.exelist[0])) + compiler.extend(cls.use_linker_args(cc.linker.exelist[0])) extra_args['direct'] = True extra_args['machine'] = cc.linker.machine else: @@ -984,7 +991,7 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust if 'ccache' in exelist[0]: del exelist[0] c = exelist.pop(0) - compiler.extend(RustCompiler.use_linker_args(c)) + compiler.extend(cls.use_linker_args(c)) # Also ensure that we pass any extra arguments to the linker for l in exelist: @@ -1002,12 +1009,12 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust **extra_args) # type: ignore elif 'link' in override[0]: linker = guess_win_linker(env, - override, RustCompiler, for_machine, use_linker_prefix=False) + override, cls, for_machine, use_linker_prefix=False) # rustc takes linker arguments without a prefix, and # inserts the correct prefix itself. assert isinstance(linker, VisualStudioLikeLinkerMixin) linker.direct = True - compiler.extend(RustCompiler.use_linker_args(linker.exelist[0])) + compiler.extend(cls.use_linker_args(linker.exelist[0])) else: # On linux and macos rust will invoke the c compiler for # linking, on windows it will use lld-link or link.exe. @@ -1019,10 +1026,10 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust # Of course, we're not going to use any of that, we just # need it to get the proper arguments to pass to rustc c = linker.exelist[1] if linker.exelist[0].endswith('ccache') else linker.exelist[0] - compiler.extend(RustCompiler.use_linker_args(c)) + compiler.extend(cls.use_linker_args(c)) - env.coredata.add_lang_args(RustCompiler.language, RustCompiler, for_machine, env) - return RustCompiler( + env.coredata.add_lang_args(cls.language, cls, for_machine, env) + return cls( compiler, version, for_machine, is_cross, info, exe_wrap, linker=linker) diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 2337ceb02..9423b2d89 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -196,3 +196,20 @@ class RustCompiler(Compiler): # Rustc currently has no way to toggle this, it's controlled by whether # pic is on by rustc return [] + + +class ClippyRustCompiler(RustCompiler): + + """Clippy is a linter that wraps Rustc. + + This just provides us a different id + """ + + def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, + is_cross: bool, info: 'MachineInfo', + exe_wrapper: T.Optional['ExternalProgram'] = None, + full_version: T.Optional[str] = None, + linker: T.Optional['DynamicLinker'] = None): + super().__init__(exelist, version, for_machine, is_cross, info, + exe_wrapper, full_version, linker) + self.id = 'clippy-driver rustc' diff --git a/test cases/rust/1 basic/clippy.toml b/test cases/rust/1 basic/clippy.toml new file mode 100644 index 000000000..e9ac31b69 --- /dev/null +++ b/test cases/rust/1 basic/clippy.toml @@ -0,0 +1 @@ +blacklisted-names = ["foo"] diff --git a/test cases/rust/1 basic/prog.rs b/test cases/rust/1 basic/prog.rs index b171a80c2..f1b3d303d 100644 --- a/test cases/rust/1 basic/prog.rs +++ b/test cases/rust/1 basic/prog.rs @@ -1,3 +1,4 @@ fn main() { - println!("rust compiler is working"); + let foo = "rust compiler is working"; + println!("{}", foo); } diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 7afa98977..93a2e49b9 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -4034,3 +4034,16 @@ class AllPlatformTests(BasePlatformTests): for file, details in files.items(): with self.subTest(key='{}.{}'.format(data_type, file)): self.assertEqual(res[data_type][file], details) + + @skip_if_not_language('rust') + @unittest.skipIf(not shutil.which('clippy-driver'), 'Test requires clippy-driver') + def test_rust_clippy(self) -> None: + if self.backend is not Backend.ninja: + raise unittest.SkipTest('Rust is only supported with ninja currently') + # Wehn clippy is used, we should get an exception since a variable named + # "foo" is used, but is on our denylist + testdir = os.path.join(self.rust_test_dir, '1 basic') + self.init(testdir, extra_args=['--werror'], override_envvars={'RUSTC': 'clippy-driver'}) + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.build() + self.assertIn('error: use of a blacklisted/placeholder name `foo`', cm.exception.stdout) diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py index 349278537..93713957f 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -69,6 +69,7 @@ class BasePlatformTests(TestCase): self.uninstall_command = get_backend_commands(self.backend) # Test directories self.common_test_dir = os.path.join(src_root, 'test cases/common') + self.rust_test_dir = os.path.join(src_root, 'test cases/rust') self.vala_test_dir = os.path.join(src_root, 'test cases/vala') self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') self.unit_test_dir = os.path.join(src_root, 'test cases/unit') @@ -135,7 +136,7 @@ class BasePlatformTests(TestCase): os.environ.update(self.orig_env) super().tearDown() - def _run(self, command, *, workdir=None, override_envvars=None): + def _run(self, command, *, workdir=None, override_envvars: T.Optional[T.Mapping[str, str]] = None): ''' Run a command while printing the stdout and stderr to stdout, and also return a copy of it @@ -164,7 +165,7 @@ class BasePlatformTests(TestCase): extra_args=None, default_args=True, inprocess=False, - override_envvars=None, + override_envvars: T.Optional[T.Mapping[str, str]] = None, workdir=None, allow_fail: bool = False) -> str: """Call `meson setup`