#![allow(clippy::unwrap_used)]
use super::{
common::{self, clippy_repository},
Backup,
};
use crate::opts;
use anyhow::{Context, Result};
use dylint_internal::git2::Oid;
use rewriter::{interface::Span as _, LineColumn, Rewriter, Span};
use std::{
collections::{BTreeMap, HashMap},
fs::{read_to_string, write},
ops::Range,
path::Path,
};
mod tokenization;
use tokenization::tokenize_lines;
mod highlight;
use highlight::{collect_highlights, Highlight};
mod rewrite;
use rewrite::{collect_rewrites, Rewrite};
mod short_id;
use short_id::ShortId;
#[derive(Debug)]
struct ReplacementSource<'rewrite> {
score: usize,
span: Span,
oid: Oid,
rewrite: &'rewrite Rewrite,
}
type ReplacementSourceMap<'rewrite> = BTreeMap<String, ReplacementSource<'rewrite>>;
enum Reason<'rewrite> {
None,
Multiple(usize, ReplacementSourceMap<'rewrite>),
}
pub fn auto_correct(
opts: &opts::Dylint,
upgrade_opts: &opts::Upgrade,
old_channel: &str,
new_oid: Oid,
) -> Result<()> {
let mut backups = BTreeMap::new();
auto_correct_revertible(opts, upgrade_opts, old_channel, new_oid, &mut backups)?;
for (file_name, mut backup) in backups {
backup
.disable()
.with_context(|| format!("Could not disable `{file_name}` backup"))?;
}
Ok(())
}
#[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
#[allow(clippy::module_name_repetitions, clippy::similar_names)]
pub fn auto_correct_revertible(
opts: &opts::Dylint,
upgrade_opts: &opts::Upgrade,
old_channel: &str,
new_oid: Oid,
backups: &mut BTreeMap<String, Backup>,
) -> Result<()> {
let path = Path::new(&upgrade_opts.path);
let mut highlights = collect_highlights(opts, path)?;
if highlights.is_empty() {
return Ok(());
}
let repository = clippy_repository(opts.quiet)?;
let rewrites = collect_rewrites(old_channel, new_oid, &repository)?;
loop {
let mut rewriters = BTreeMap::new();
let mut unrewritable_highlights = Vec::new();
for highlight in &highlights {
if !rewriters.contains_key(&highlight.file_name) {
if !backups.contains_key(&highlight.file_name) {
let backup = Backup::new(path.join(&highlight.file_name))
.with_context(|| format!("Could not backup `{}`", highlight.file_name))?;
backups.insert(highlight.file_name.clone(), backup);
}
let contents =
read_to_string(path.join(&highlight.file_name)).with_context(|| {
format!("`read_to_string` failed for `{}`", highlight.file_name)
})?;
let rewriter = Rewriter::new(contents.leak());
rewriters.insert(highlight.file_name.clone(), (0, rewriter));
}
let (last_rewritten_line, rewriter) = rewriters.get_mut(&highlight.file_name).unwrap();
if highlight.line_start <= *last_rewritten_line {
continue;
}
let mut replacement_source_map = applicable_rewrites(&rewrites, highlight)?;
let Some(best_score) = replacement_source_map
.values()
.map(|source| source.score)
.max()
else {
if highlight.is_primary {
unrewritable_highlights.push((highlight, Reason::None));
}
continue;
};
replacement_source_map.retain(|_, source| source.score == best_score);
if replacement_source_map.len() > 1 {
if highlight.is_primary {
unrewritable_highlights.push((
highlight,
Reason::Multiple(best_score, replacement_source_map),
));
}
continue;
}
let (replacement, source) = replacement_source_map.pop_first().unwrap();
eprintln!(
"Rewriting with score {} rewrite from {}: {:#?} {:#?}",
best_score,
source.oid.short_id(),
source.rewrite,
highlight,
);
*last_rewritten_line = source.span.end().line;
let _: String = rewriter.rewrite(&source.span, &replacement);
}
for (file_name, (_, rewriter)) in rewriters {
let contents = rewriter.contents();
write(path.join(&file_name), contents)
.with_context(|| format!("`write` failed for `{file_name}`"))?;
}
if !unrewritable_highlights.is_empty() {
display_unrewritable(&unrewritable_highlights);
return Ok(());
}
highlights = collect_highlights(opts, path)?;
if highlights.is_empty() {
return Ok(());
}
}
}
fn applicable_rewrites<'rewrite>(
rewrites: &'rewrite HashMap<Rewrite, Oid>,
highlight: &Highlight,
) -> Result<ReplacementSourceMap<'rewrite>> {
let mut replacement_source_map = ReplacementSourceMap::new();
for (rewrite, &oid) in rewrites {
if let Some((score, offset)) = rewrite.applicability(highlight) {
let (_, replacement) = span_and_text_of_tokens(
1,
&rewrite.new_lines,
rewrite.common_prefix_len..rewrite.new_tokens.len() - rewrite.common_suffix_len,
)?;
let (span, _) = span_and_text_of_tokens(
highlight.line_start,
&highlight.lines,
offset
..offset
+ (rewrite.old_tokens.len()
- rewrite.common_suffix_len
- rewrite.common_prefix_len),
)?;
if let Some(source) = replacement_source_map.get_mut(&replacement) {
if source.score < score {
*source = ReplacementSource {
score,
span,
oid,
rewrite,
};
}
} else {
replacement_source_map.insert(
replacement.clone(),
ReplacementSource {
score,
span,
oid,
rewrite,
},
);
}
}
}
Ok(replacement_source_map)
}
#[cfg_attr(dylint_lib = "supplementary", allow(commented_code))]
pub fn span_and_text_of_tokens<S: AsRef<str>>(
line_start: usize,
lines: &[S],
range: Range<usize>,
) -> Result<(Span, String)> {
if range.is_empty() {
return Ok(Default::default());
}
let lines = lines.iter().map(AsRef::as_ref).collect::<Vec<_>>();
let lines_orig = &lines;
let tokens = tokenize_lines(&lines)?;
let mut lines = lines.iter();
let mut line = lines.next().copied().unwrap();
let mut i_token = 0;
let mut start = None;
let mut line_column = LineColumn {
line: line_start,
column: 0,
};
let mut text = String::new();
while i_token < range.end {
if line.as_bytes().iter().all(u8::is_ascii_whitespace) {
if range.start < i_token {
text += line;
text += "\n";
}
line = lines.next().unwrap();
line_column.line += 1;
line_column.column = 0;
continue;
}
let token = tokens[i_token];
let len = token.len();
#[allow(clippy::panic)]
let offset = line
.find(token)
.unwrap_or_else(|| panic!("Could not find token {token:?} in line {line:?}"));
assert!(line[..offset]
.as_bytes()
.iter()
.all(u8::is_ascii_whitespace));
if range.start < i_token {
text += &line[..offset];
}
line = &line[offset..];
line_column.column += offset;
if range.start == i_token {
start = Some(line_column);
}
assert!(line.starts_with(token));
if range.start <= i_token {
text += token;
}
line = &line[len..];
line_column.column += len;
i_token += 1;
}
#[allow(clippy::panic)]
let start = start.unwrap_or_else(|| {
panic!(
"`start` was not set for {:#?}",
(line_start, lines_orig, line, &text, range),
)
});
Ok((Span::new(start, line_column), text))
}
fn display_unrewritable(unrewritable: &[(&Highlight, Reason)]) {
for (highlight, reason) in unrewritable {
assert!(highlight.is_primary);
match reason {
Reason::None => {
eprintln!("Found no applicable rewrites for {highlight:#?}");
}
Reason::Multiple(score, rewrites) => {
eprintln!(
"Found multiple rewrites with score {score} for {highlight:#?}: {rewrites:#?}"
);
}
}
}
}