Line data Source code
1 : #![feature(rustc_private)]
2 : #![deny(clippy::expect_used)]
3 : #![deny(clippy::unwrap_used)]
4 : #![deny(clippy::panic)]
5 : #![deny(unused_extern_crates)]
6 :
7 : extern crate rustc_driver;
8 : extern crate rustc_interface;
9 : extern crate rustc_lint;
10 : extern crate rustc_session;
11 : extern crate rustc_span;
12 :
13 : use anyhow::{bail, ensure, Result};
14 : use dylint_internal::{env, parse_path_filename, rustup::is_rustc};
15 : use std::{
16 : collections::BTreeSet,
17 : ffi::{CString, OsStr},
18 : path::{Path, PathBuf},
19 : };
20 :
21 : pub const DYLINT_VERSION: &str = "0.1.0";
22 :
23 : type DylintVersionFunc = unsafe fn() -> *mut std::os::raw::c_char;
24 :
25 : type RegisterLintsFunc =
26 : unsafe fn(sess: &rustc_session::Session, store: &mut rustc_lint::LintStore);
27 :
28 : /// A library that has been loaded but whose lints have not necessarily been registered.
29 : struct LoadedLibrary {
30 : path: PathBuf,
31 : lib: libloading::Library,
32 : }
33 :
34 : #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
35 : struct Lint {
36 : name: &'static str,
37 : level: rustc_lint::Level,
38 : desc: &'static str,
39 : }
40 :
41 : impl From<&rustc_lint::Lint> for Lint {
42 0 : fn from(lint: &rustc_lint::Lint) -> Self {
43 0 : Self {
44 0 : name: lint.name,
45 0 : level: lint.default_level,
46 0 : desc: lint.desc,
47 0 : }
48 0 : }
49 : }
50 :
51 : impl LoadedLibrary {
52 0 : fn register_lints(
53 0 : &self,
54 0 : sess: &rustc_session::Session,
55 0 : lint_store: &mut rustc_lint::LintStore,
56 0 : ) {
57 0 : (|| unsafe {
58 0 : if let Ok(func) = self.lib.get::<DylintVersionFunc>(b"dylint_version") {
59 0 : let dylint_version = CString::from_raw(func()).into_string()?;
60 0 : ensure!(
61 0 : dylint_version == DYLINT_VERSION,
62 0 : "`{}` has dylint version `{}`, but `{}` was expected",
63 0 : self.path.to_string_lossy(),
64 : dylint_version,
65 : DYLINT_VERSION
66 : );
67 : } else {
68 0 : bail!(
69 0 : "could not find `dylint_version` in `{}`",
70 0 : self.path.to_string_lossy()
71 0 : );
72 : }
73 0 : if let Ok(func) = self.lib.get::<RegisterLintsFunc>(b"register_lints") {
74 0 : func(sess, lint_store);
75 0 : } else {
76 0 : bail!(
77 0 : "could not find `register_lints` in `{}`",
78 0 : self.path.to_string_lossy()
79 0 : );
80 : }
81 0 : Ok(())
82 0 : })()
83 0 : .unwrap_or_else(|err| {
84 0 : session_err(sess, &err);
85 0 : });
86 0 : }
87 : }
88 :
89 : #[rustversion::before(2023-12-18)]
90 : fn session_err(sess: &rustc_session::Session, err: &impl ToString) -> rustc_span::ErrorGuaranteed {
91 : sess.diagnostic().err(err.to_string())
92 : }
93 :
94 : #[rustversion::since(2023-12-18)]
95 0 : fn session_err(sess: &rustc_session::Session, err: &impl ToString) -> rustc_span::ErrorGuaranteed {
96 0 : sess.dcx().err(err.to_string())
97 0 : }
98 :
99 : struct Callbacks {
100 : loaded_libs: Vec<LoadedLibrary>,
101 : }
102 :
103 : // smoelius: Use of thread local storage was added to Clippy by:
104 : // https://github.com/rust-lang/rust-clippy/commit/3c06e0b1ce003912f8fe0536d3a7fe22558e38cf
105 : // This results in a segfault after the Clippy library has been unloaded; see the following issue
106 : // for an explanation as to why: https://github.com/nagisa/rust_libloading/issues/5
107 : // The workaround I've chosen is:
108 : // https://github.com/nagisa/rust_libloading/issues/5#issuecomment-244195096
109 : impl Callbacks {
110 : // smoelius: Load the libraries when `Callbacks` is created and not later (e.g., in `config`)
111 : // to ensure that the libraries live long enough.
112 0 : fn new(paths: Vec<PathBuf>) -> Self {
113 0 : let mut loaded_libs = Vec::new();
114 0 : for path in paths {
115 0 : unsafe {
116 0 : // smoelius: `libloading` does not define `RTLD_NODELETE`.
117 0 : #[cfg(unix)]
118 0 : let result = libloading::os::unix::Library::open(
119 0 : Some(&path),
120 0 : libloading::os::unix::RTLD_LAZY
121 0 : | libloading::os::unix::RTLD_LOCAL
122 0 : | libc::RTLD_NODELETE,
123 0 : )
124 0 : .map(Into::into);
125 0 :
126 0 : #[cfg(not(unix))]
127 0 : let result = libloading::Library::new(&path);
128 0 :
129 0 : let lib = result.unwrap_or_else(|err| {
130 0 : // smoelius: rust-lang/rust#111633 changed the type of `early_error`'s `msg`
131 0 : // argument from `&str` to `impl Into<DiagnosticMessage>`.
132 0 : // smoelius: And rust-lang/rust#111748 made it that `msg` is borrowed for
133 0 : // `'static`. Since the program is about to exit, it's probably fine to leak the
134 0 : // string.
135 0 : let msg = format!(
136 0 : "could not load library `{}`: {}",
137 0 : path.to_string_lossy(),
138 0 : err
139 0 : );
140 0 : early_error(msg);
141 0 : });
142 0 :
143 0 : loaded_libs.push(LoadedLibrary { path, lib });
144 0 : }
145 : }
146 0 : Self { loaded_libs }
147 0 : }
148 : }
149 :
150 : #[rustversion::before(2023-06-28)]
151 : fn early_error(msg: String) -> ! {
152 : rustc_session::early_error(
153 : rustc_session::config::ErrorOutputType::default(),
154 : Box::leak(msg.into_boxed_str()) as &str,
155 : )
156 : }
157 :
158 : #[rustversion::since(2023-06-28)]
159 : extern crate rustc_errors;
160 :
161 : #[rustversion::all(since(2023-06-28), before(2023-12-18))]
162 : fn early_error(msg: impl Into<rustc_errors::DiagnosticMessage>) -> ! {
163 : let handler =
164 : rustc_session::EarlyErrorHandler::new(rustc_session::config::ErrorOutputType::default());
165 : handler.early_error(msg)
166 : }
167 :
168 : #[rustversion::all(since(2023-12-18), before(2023-12-23))]
169 : fn early_error(msg: impl Into<rustc_errors::DiagnosticMessage>) -> ! {
170 : let handler =
171 : rustc_session::EarlyDiagCtxt::new(rustc_session::config::ErrorOutputType::default());
172 : handler.early_error(msg)
173 : }
174 :
175 : #[rustversion::all(since(2023-12-23), before(2024-03-05))]
176 : fn early_error(msg: impl Into<rustc_errors::DiagnosticMessage>) -> ! {
177 : let handler =
178 : rustc_session::EarlyDiagCtxt::new(rustc_session::config::ErrorOutputType::default());
179 : handler.early_fatal(msg)
180 : }
181 :
182 : #[rustversion::since(2024-03-05)]
183 0 : fn early_error(msg: impl Into<rustc_errors::DiagMessage>) -> ! {
184 0 : let handler =
185 0 : rustc_session::EarlyDiagCtxt::new(rustc_session::config::ErrorOutputType::default());
186 0 : handler.early_fatal(msg)
187 : }
188 :
189 : trait ParseSess {
190 : fn parse_sess(&self) -> &rustc_session::parse::ParseSess;
191 : }
192 :
193 : impl ParseSess for rustc_session::Session {
194 : #[rustversion::before(2024-03-05)]
195 : fn parse_sess(&self) -> &rustc_session::parse::ParseSess {
196 : &self.parse_sess
197 : }
198 :
199 : #[rustversion::since(2024-03-05)]
200 0 : fn parse_sess(&self) -> &rustc_session::parse::ParseSess {
201 0 : &self.psess
202 0 : }
203 : }
204 :
205 : #[rustversion::before(2022-07-14)]
206 : fn zero_mir_opt_level(config: &mut rustc_interface::Config) {
207 : trait Zeroable {
208 : fn zero(&mut self);
209 : }
210 :
211 : impl Zeroable for usize {
212 : fn zero(&mut self) {
213 : *self = 0;
214 : }
215 : }
216 :
217 : impl Zeroable for Option<usize> {
218 : fn zero(&mut self) {
219 : *self = Some(0);
220 : }
221 : }
222 :
223 : // smoelius: `Zeroable` is a hack to make the next line compile for different Rust versions:
224 : // https://github.com/rust-lang/rust-clippy/commit/0941fc0bb5d655cdd0816f862af8cfe70556dad6
225 : config.opts.debugging_opts.mir_opt_level.zero();
226 : }
227 :
228 : // smoelius: Relevant PR and merge commit:
229 : // - https://github.com/rust-lang/rust/pull/98975
230 : // - https://github.com/rust-lang/rust/commit/0ed9c64c3e63acac9bd77abce62501696c390450
231 : #[rustversion::since(2022-07-14)]
232 0 : fn zero_mir_opt_level(config: &mut rustc_interface::Config) {
233 0 : config.opts.unstable_opts.mir_opt_level = Some(0);
234 0 : }
235 :
236 : impl rustc_driver::Callbacks for Callbacks {
237 0 : fn config(&mut self, config: &mut rustc_interface::Config) {
238 0 : let previous = config.register_lints.take();
239 0 : let loaded_libs = self.loaded_libs.split_off(0);
240 0 : config.register_lints = Some(Box::new(move |sess, lint_store| {
241 0 : if let Some(previous) = &previous {
242 0 : previous(sess, lint_store);
243 0 : }
244 :
245 0 : let dylint_libs = env::var(env::DYLINT_LIBS).ok();
246 0 : let dylint_metadata = env::var(env::DYLINT_METADATA).ok();
247 0 : let dylint_no_deps = env::var(env::DYLINT_NO_DEPS).ok();
248 0 : let dylint_no_deps_enabled =
249 0 : dylint_no_deps.as_ref().map_or(false, |value| value != "0");
250 0 : let cargo_primary_package_is_set = env::var(env::CARGO_PRIMARY_PACKAGE).is_ok();
251 0 :
252 0 : sess.parse_sess().env_depinfo.lock().insert((
253 0 : rustc_span::Symbol::intern(env::DYLINT_LIBS),
254 0 : dylint_libs.as_deref().map(rustc_span::Symbol::intern),
255 0 : ));
256 0 : sess.parse_sess().env_depinfo.lock().insert((
257 0 : rustc_span::Symbol::intern(env::DYLINT_METADATA),
258 0 : dylint_metadata.as_deref().map(rustc_span::Symbol::intern),
259 0 : ));
260 0 : sess.parse_sess().env_depinfo.lock().insert((
261 0 : rustc_span::Symbol::intern(env::DYLINT_NO_DEPS),
262 0 : dylint_no_deps.as_deref().map(rustc_span::Symbol::intern),
263 0 : ));
264 0 :
265 0 : if dylint_no_deps_enabled && !cargo_primary_package_is_set {
266 0 : return;
267 0 : }
268 0 :
269 0 : let mut before = BTreeSet::<Lint>::new();
270 0 : if list_enabled() {
271 0 : lint_store.get_lints().iter().for_each(|&lint| {
272 0 : before.insert(lint.into());
273 0 : });
274 0 : }
275 0 : for loaded_lib in &loaded_libs {
276 0 : if let Some(path) = loaded_lib.path.to_str() {
277 0 : sess.parse_sess()
278 0 : .file_depinfo
279 0 : .lock()
280 0 : .insert(rustc_span::Symbol::intern(path));
281 0 : }
282 0 : loaded_lib.register_lints(sess, lint_store);
283 : }
284 0 : if list_enabled() {
285 0 : let mut after = BTreeSet::<Lint>::new();
286 0 : lint_store.get_lints().iter().for_each(|&lint| {
287 0 : after.insert(lint.into());
288 0 : });
289 0 : list_lints(&before, &after);
290 0 : std::process::exit(0);
291 0 : }
292 0 : }));
293 0 :
294 0 : // smoelius: Choose to be compatible with Clippy:
295 0 : // https://github.com/rust-lang/rust-clippy/commit/7bae5bd828e98af9d245b77118c075a7f1a036b9
296 0 : zero_mir_opt_level(config);
297 0 : }
298 : }
299 :
300 : #[must_use]
301 0 : fn list_enabled() -> bool {
302 0 : env::var(env::DYLINT_LIST).map_or(false, |value| value != "0")
303 0 : }
304 :
305 0 : fn list_lints(before: &BTreeSet<Lint>, after: &BTreeSet<Lint>) {
306 0 : let difference: Vec<Lint> = after.difference(before).cloned().collect();
307 0 :
308 0 : let name_width = difference
309 0 : .iter()
310 0 : .map(|lint| lint.name.len())
311 0 : .max()
312 0 : .unwrap_or_default();
313 0 :
314 0 : let level_width = difference
315 0 : .iter()
316 0 : .map(|lint| lint.level.as_str().len())
317 0 : .max()
318 0 : .unwrap_or_default();
319 :
320 0 : for Lint { name, level, desc } in difference {
321 0 : println!(
322 0 : " {:<name_width$} {:<level_width$} {}",
323 0 : name.to_lowercase(),
324 0 : level.as_str(),
325 0 : desc,
326 0 : name_width = name_width,
327 0 : level_width = level_width
328 0 : );
329 0 : }
330 0 : }
331 :
332 0 : pub fn dylint_driver<T: AsRef<OsStr>>(args: &[T]) -> Result<()> {
333 0 : if args.len() <= 1 || args.iter().any(|arg| arg.as_ref() == "-V") {
334 0 : println!("{} {}", env!("RUSTUP_TOOLCHAIN"), env!("CARGO_PKG_VERSION"));
335 0 : return Ok(());
336 0 : }
337 0 :
338 0 : run(&args[1..])
339 0 : }
340 :
341 0 : pub fn run<T: AsRef<OsStr>>(args: &[T]) -> Result<()> {
342 0 : let sysroot = sysroot().ok();
343 0 : let rustflags = rustflags();
344 0 : let paths = paths();
345 :
346 0 : let rustc_args = rustc_args(args, &sysroot, &rustflags, &paths)?;
347 :
348 0 : let mut callbacks = Callbacks::new(paths);
349 0 :
350 0 : // smoelius: I am not sure that this should be here. `RUST_LOG=debug cargo test` fails because
351 0 : // of the log messages.
352 0 : log::debug!("{:?}", rustc_args);
353 :
354 0 : rustc_driver::RunCompiler::new(&rustc_args, &mut callbacks)
355 0 : .run()
356 0 : .map_err(|_| std::process::exit(1))
357 0 : }
358 :
359 0 : fn sysroot() -> Result<PathBuf> {
360 0 : let rustup_home = env::var(env::RUSTUP_HOME)?;
361 0 : let rustup_toolchain = env::var(env::RUSTUP_TOOLCHAIN)?;
362 0 : Ok(PathBuf::from(rustup_home)
363 0 : .join("toolchains")
364 0 : .join(rustup_toolchain))
365 0 : }
366 :
367 0 : fn rustflags() -> Vec<String> {
368 0 : env::var(env::DYLINT_RUSTFLAGS).map_or_else(
369 0 : |_| Vec::new(),
370 0 : |rustflags| rustflags.split_whitespace().map(String::from).collect(),
371 0 : )
372 0 : }
373 :
374 0 : fn paths() -> Vec<PathBuf> {
375 0 : (|| -> Result<_> {
376 0 : let dylint_libs = env::var(env::DYLINT_LIBS)?;
377 0 : serde_json::from_str(&dylint_libs).map_err(Into::into)
378 0 : })()
379 0 : .unwrap_or_default()
380 0 : }
381 :
382 3 : fn rustc_args<T: AsRef<OsStr>, U: AsRef<str>, V: AsRef<Path>>(
383 3 : args: &[T],
384 3 : sysroot: &Option<PathBuf>,
385 3 : rustflags: &[U],
386 3 : paths: &[V],
387 3 : ) -> Result<Vec<String>> {
388 3 : let mut args = args.iter().peekable();
389 3 : let mut rustc_args = Vec::new();
390 3 :
391 3 : let first_arg = args.peek();
392 3 : if first_arg.map_or(true, |arg| !is_rustc(arg)) {
393 1 : rustc_args.push("rustc".to_owned());
394 2 : }
395 3 : if let Some(arg) = first_arg {
396 3 : if is_rustc(arg) {
397 2 : rustc_args.push(arg.as_ref().to_string_lossy().to_string());
398 2 : let _ = args.next();
399 2 : }
400 0 : }
401 3 : if let Some(sysroot) = sysroot {
402 0 : rustc_args.extend([
403 0 : "--sysroot".to_owned(),
404 0 : sysroot.to_string_lossy().to_string(),
405 0 : ]);
406 3 : }
407 3 : for path in paths {
408 0 : if let Some((name, _)) = parse_path_filename(path.as_ref()) {
409 0 : rustc_args.push(format!(r#"--cfg=dylint_lib="{name}""#));
410 0 : } else {
411 0 : bail!("could not parse `{}`", path.as_ref().to_string_lossy());
412 : }
413 : }
414 6 : rustc_args.extend(args.map(|s| s.as_ref().to_string_lossy().to_string()));
415 3 : rustc_args.extend(
416 3 : rustflags
417 3 : .iter()
418 3 : .map(|rustflag| rustflag.as_ref().to_owned()),
419 3 : );
420 3 :
421 3 : Ok(rustc_args)
422 3 : }
423 :
424 : #[expect(clippy::unwrap_used)]
425 : #[cfg(test)]
426 : mod test {
427 : use super::*;
428 : use rustc_version::{version_meta, Channel};
429 :
430 : #[test]
431 1 : fn channel_is_nightly() {
432 1 : assert!(matches!(version_meta().unwrap().channel, Channel::Nightly));
433 1 : }
434 :
435 : #[test]
436 1 : fn no_rustc() {
437 1 : assert_eq!(
438 1 : rustc_args(
439 1 : &["--crate-name", "name"],
440 1 : &None,
441 1 : &[] as &[&str],
442 1 : &[] as &[&Path]
443 1 : )
444 1 : .unwrap(),
445 1 : vec!["rustc", "--crate-name", "name"]
446 1 : );
447 1 : }
448 :
449 : #[test]
450 1 : fn plain_rustc() {
451 1 : assert_eq!(
452 1 : rustc_args(
453 1 : &["rustc", "--crate-name", "name"],
454 1 : &None,
455 1 : &[] as &[&str],
456 1 : &[] as &[&Path]
457 1 : )
458 1 : .unwrap(),
459 1 : vec!["rustc", "--crate-name", "name"]
460 1 : );
461 1 : }
462 :
463 : #[test]
464 1 : fn qualified_rustc() {
465 1 : assert_eq!(
466 1 : rustc_args(
467 1 : &["/bin/rustc", "--crate-name", "name"],
468 1 : &None,
469 1 : &[] as &[&str],
470 1 : &[] as &[&Path]
471 1 : )
472 1 : .unwrap(),
473 1 : vec!["/bin/rustc", "--crate-name", "name"]
474 1 : );
475 1 : }
476 : }
|