335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
use crate::utils::{
|
|
ErrAction, FileUpdater, UpdateMode, UpdateStatus, expect_action, run_with_output, split_args_for_threads,
|
|
walk_dir_no_dot_or_target,
|
|
};
|
|
use itertools::Itertools;
|
|
use rustc_lexer::{TokenKind, tokenize};
|
|
use std::fmt::Write;
|
|
use std::fs;
|
|
use std::io::{self, Read};
|
|
use std::ops::ControlFlow;
|
|
use std::path::PathBuf;
|
|
use std::process::{self, Command, Stdio};
|
|
|
|
pub enum Error {
|
|
Io(io::Error),
|
|
Parse(PathBuf, usize, String),
|
|
CheckFailed,
|
|
}
|
|
|
|
impl From<io::Error> for Error {
|
|
fn from(error: io::Error) -> Self {
|
|
Self::Io(error)
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
fn display(&self) {
|
|
match self {
|
|
Self::CheckFailed => {
|
|
eprintln!("Formatting check failed!\nRun `cargo dev fmt` to update.");
|
|
},
|
|
Self::Io(err) => {
|
|
eprintln!("error: {err}");
|
|
},
|
|
Self::Parse(path, line, msg) => {
|
|
eprintln!("error parsing `{}:{line}`: {msg}", path.display());
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ClippyConf<'a> {
|
|
name: &'a str,
|
|
attrs: &'a str,
|
|
lints: Vec<&'a str>,
|
|
field: &'a str,
|
|
}
|
|
|
|
fn offset_to_line(text: &str, offset: usize) -> usize {
|
|
match text.split('\n').try_fold((1usize, 0usize), |(line, pos), s| {
|
|
let pos = pos + s.len() + 1;
|
|
if pos > offset {
|
|
ControlFlow::Break(line)
|
|
} else {
|
|
ControlFlow::Continue((line + 1, pos))
|
|
}
|
|
}) {
|
|
ControlFlow::Break(x) | ControlFlow::Continue((x, _)) => x,
|
|
}
|
|
}
|
|
|
|
/// Formats the configuration list in `clippy_config/src/conf.rs`
|
|
#[expect(clippy::too_many_lines)]
|
|
fn fmt_conf(check: bool) -> Result<(), Error> {
|
|
#[derive(Clone, Copy)]
|
|
enum State {
|
|
Start,
|
|
Docs,
|
|
Pound,
|
|
OpenBracket,
|
|
Attr(u32),
|
|
Lints,
|
|
EndLints,
|
|
Field,
|
|
}
|
|
|
|
let path = "clippy_config/src/conf.rs";
|
|
let text = fs::read_to_string(path)?;
|
|
|
|
let (pre, conf) = text
|
|
.split_once("define_Conf! {\n")
|
|
.expect("can't find config definition");
|
|
let (conf, post) = conf.split_once("\n}\n").expect("can't find config definition");
|
|
let conf_offset = pre.len() + 15;
|
|
|
|
let mut pos = 0u32;
|
|
let mut attrs_start = 0;
|
|
let mut attrs_end = 0;
|
|
let mut field_start = 0;
|
|
let mut lints = Vec::new();
|
|
let mut name = "";
|
|
let mut fields = Vec::new();
|
|
let mut state = State::Start;
|
|
|
|
for (i, t) in tokenize(conf)
|
|
.map(|x| {
|
|
let start = pos;
|
|
pos += x.len;
|
|
(start as usize, x)
|
|
})
|
|
.filter(|(_, t)| !matches!(t.kind, TokenKind::Whitespace))
|
|
{
|
|
match (state, t.kind) {
|
|
(State::Start, TokenKind::LineComment { doc_style: Some(_) }) => {
|
|
attrs_start = i;
|
|
attrs_end = i + t.len as usize;
|
|
state = State::Docs;
|
|
},
|
|
(State::Start, TokenKind::Pound) => {
|
|
attrs_start = i;
|
|
attrs_end = i;
|
|
state = State::Pound;
|
|
},
|
|
(State::Docs, TokenKind::LineComment { doc_style: Some(_) }) => attrs_end = i + t.len as usize,
|
|
(State::Docs, TokenKind::Pound) => state = State::Pound,
|
|
(State::Pound, TokenKind::OpenBracket) => state = State::OpenBracket,
|
|
(State::OpenBracket, TokenKind::Ident) => {
|
|
state = if conf[i..i + t.len as usize] == *"lints" {
|
|
State::Lints
|
|
} else {
|
|
State::Attr(0)
|
|
};
|
|
},
|
|
(State::Attr(0), TokenKind::CloseBracket) => {
|
|
attrs_end = i + 1;
|
|
state = State::Docs;
|
|
},
|
|
(State::Attr(x), TokenKind::OpenParen | TokenKind::OpenBracket | TokenKind::OpenBrace) => {
|
|
state = State::Attr(x + 1);
|
|
},
|
|
(State::Attr(x), TokenKind::CloseParen | TokenKind::CloseBracket | TokenKind::CloseBrace) => {
|
|
state = State::Attr(x - 1);
|
|
},
|
|
(State::Lints, TokenKind::Ident) => lints.push(&conf[i..i + t.len as usize]),
|
|
(State::Lints, TokenKind::CloseBracket) => state = State::EndLints,
|
|
(State::EndLints | State::Docs, TokenKind::Ident) => {
|
|
field_start = i;
|
|
name = &conf[i..i + t.len as usize];
|
|
state = State::Field;
|
|
},
|
|
(State::Field, TokenKind::LineComment { doc_style: Some(_) }) => {
|
|
#[expect(clippy::drain_collect)]
|
|
fields.push(ClippyConf {
|
|
name,
|
|
attrs: &conf[attrs_start..attrs_end],
|
|
lints: lints.drain(..).collect(),
|
|
field: conf[field_start..i].trim_end(),
|
|
});
|
|
attrs_start = i;
|
|
attrs_end = i + t.len as usize;
|
|
state = State::Docs;
|
|
},
|
|
(State::Field, TokenKind::Pound) => {
|
|
#[expect(clippy::drain_collect)]
|
|
fields.push(ClippyConf {
|
|
name,
|
|
attrs: &conf[attrs_start..attrs_end],
|
|
lints: lints.drain(..).collect(),
|
|
field: conf[field_start..i].trim_end(),
|
|
});
|
|
attrs_start = i;
|
|
attrs_end = i;
|
|
state = State::Pound;
|
|
},
|
|
(State::Field | State::Attr(_), _)
|
|
| (State::Lints, TokenKind::Comma | TokenKind::OpenParen | TokenKind::CloseParen) => {},
|
|
_ => {
|
|
return Err(Error::Parse(
|
|
PathBuf::from(path),
|
|
offset_to_line(&text, conf_offset + i),
|
|
format!("unexpected token `{}`", &conf[i..i + t.len as usize]),
|
|
));
|
|
},
|
|
}
|
|
}
|
|
|
|
if !matches!(state, State::Field) {
|
|
return Err(Error::Parse(
|
|
PathBuf::from(path),
|
|
offset_to_line(&text, conf_offset + conf.len()),
|
|
"incomplete field".into(),
|
|
));
|
|
}
|
|
fields.push(ClippyConf {
|
|
name,
|
|
attrs: &conf[attrs_start..attrs_end],
|
|
lints,
|
|
field: conf[field_start..].trim_end(),
|
|
});
|
|
|
|
for field in &mut fields {
|
|
field.lints.sort_unstable();
|
|
}
|
|
fields.sort_by_key(|x| x.name);
|
|
|
|
let new_text = format!(
|
|
"{pre}define_Conf! {{\n{}}}\n{post}",
|
|
fields.iter().format_with("", |field, f| {
|
|
if field.lints.is_empty() {
|
|
f(&format_args!(" {}\n {}\n", field.attrs, field.field))
|
|
} else if field.lints.iter().map(|x| x.len() + 2).sum::<usize>() < 120 - 14 {
|
|
f(&format_args!(
|
|
" {}\n #[lints({})]\n {}\n",
|
|
field.attrs,
|
|
field.lints.iter().join(", "),
|
|
field.field,
|
|
))
|
|
} else {
|
|
f(&format_args!(
|
|
" {}\n #[lints({}\n )]\n {}\n",
|
|
field.attrs,
|
|
field
|
|
.lints
|
|
.iter()
|
|
.format_with("", |x, f| f(&format_args!("\n {x},"))),
|
|
field.field,
|
|
))
|
|
}
|
|
})
|
|
);
|
|
|
|
if text != new_text {
|
|
if check {
|
|
return Err(Error::CheckFailed);
|
|
}
|
|
fs::write(path, new_text)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Format the symbols list
|
|
fn fmt_syms(update_mode: UpdateMode) {
|
|
FileUpdater::default().update_file_checked(
|
|
"cargo dev fmt",
|
|
update_mode,
|
|
"clippy_utils/src/sym.rs",
|
|
&mut |_, text: &str, new_text: &mut String| {
|
|
let (pre, conf) = text.split_once("generate! {\n").expect("can't find generate! call");
|
|
let (conf, post) = conf.split_once("\n}\n").expect("can't find end of generate! call");
|
|
let mut lines = conf
|
|
.lines()
|
|
.map(|line| {
|
|
let line = line.trim();
|
|
line.strip_suffix(',').unwrap_or(line).trim_end()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
lines.sort_unstable();
|
|
write!(
|
|
new_text,
|
|
"{pre}generate! {{\n {},\n}}\n{post}",
|
|
lines.join(",\n "),
|
|
)
|
|
.unwrap();
|
|
if text == new_text {
|
|
UpdateStatus::Unchanged
|
|
} else {
|
|
UpdateStatus::Changed
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
fn run_rustfmt(update_mode: UpdateMode) {
|
|
let mut rustfmt_path = String::from_utf8(run_with_output(
|
|
"rustup which rustfmt",
|
|
Command::new("rustup").args(["which", "rustfmt"]),
|
|
))
|
|
.expect("invalid rustfmt path");
|
|
rustfmt_path.truncate(rustfmt_path.trim_end().len());
|
|
|
|
let args: Vec<_> = walk_dir_no_dot_or_target()
|
|
.filter_map(|e| {
|
|
let e = expect_action(e, ErrAction::Read, ".");
|
|
e.path()
|
|
.as_os_str()
|
|
.as_encoded_bytes()
|
|
.ends_with(b".rs")
|
|
.then(|| e.into_path().into_os_string())
|
|
})
|
|
.collect();
|
|
|
|
let mut children: Vec<_> = split_args_for_threads(
|
|
32,
|
|
|| {
|
|
let mut cmd = Command::new(&rustfmt_path);
|
|
if update_mode.is_check() {
|
|
cmd.arg("--check");
|
|
}
|
|
cmd.stdout(Stdio::null())
|
|
.stdin(Stdio::null())
|
|
.stderr(Stdio::piped())
|
|
.args(["--unstable-features", "--skip-children"]);
|
|
cmd
|
|
},
|
|
args.iter(),
|
|
)
|
|
.map(|mut cmd| expect_action(cmd.spawn(), ErrAction::Run, "rustfmt"))
|
|
.collect();
|
|
|
|
for child in &mut children {
|
|
let status = expect_action(child.wait(), ErrAction::Run, "rustfmt");
|
|
match (update_mode, status.exit_ok()) {
|
|
(UpdateMode::Check | UpdateMode::Change, Ok(())) => {},
|
|
(UpdateMode::Check, Err(_)) => {
|
|
let mut s = String::new();
|
|
if let Some(mut stderr) = child.stderr.take()
|
|
&& stderr.read_to_string(&mut s).is_ok()
|
|
{
|
|
eprintln!("{s}");
|
|
}
|
|
eprintln!("Formatting check failed!\nRun `cargo dev fmt` to update.");
|
|
process::exit(1);
|
|
},
|
|
(UpdateMode::Change, e) => {
|
|
let mut s = String::new();
|
|
if let Some(mut stderr) = child.stderr.take()
|
|
&& stderr.read_to_string(&mut s).is_ok()
|
|
{
|
|
eprintln!("{s}");
|
|
}
|
|
expect_action(e, ErrAction::Run, "rustfmt");
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// the "main" function of cargo dev fmt
|
|
pub fn run(update_mode: UpdateMode) {
|
|
run_rustfmt(update_mode);
|
|
fmt_syms(update_mode);
|
|
if let Err(e) = fmt_conf(update_mode.is_check()) {
|
|
e.display();
|
|
process::exit(1);
|
|
}
|
|
}
|