use crate::ast::visitor::{CommentInfo, CommentVisitor, VisitDecision};
use crate::config::{ConfigManager, ResolvedConfig};
use crate::languages::config::CommentSyntaxResolution;
use crate::languages::registry::LanguageRegistry;
use crate::rules::preservation::PreservationRule;
use anyhow::{Context, Result};
use std::borrow::Cow;
use std::path::Path;
use tree_sitter::Parser;
#[derive(Debug, Clone)]
pub struct ProcessingOptions {
pub remove_todo: bool,
pub remove_fixme: bool,
pub remove_doc: bool,
pub custom_preserve_patterns: Vec<String>,
pub use_default_ignores: bool,
pub dry_run: bool,
pub show_diff: bool,
pub respect_gitignore: bool,
pub traverse_git_repos: bool,
}
pub struct Processor {
parser: Parser,
registry: LanguageRegistry,
}
impl Default for Processor {
fn default() -> Self {
Self::new()
}
}
impl Processor {
pub fn new() -> Self {
Self {
parser: Parser::new(),
registry: LanguageRegistry::new(),
}
}
pub fn new_with_config(config_manager: &ConfigManager) -> Self {
let mut registry = LanguageRegistry::new();
let all_languages = config_manager.get_all_languages();
registry.register_configured_languages(&all_languages);
Self {
parser: Parser::new(),
registry,
}
}
pub fn process_file_with_config(
&mut self,
path: &Path,
config_manager: &ConfigManager,
cli_overrides: Option<&ProcessingOptions>,
) -> Result<ProcessedFile> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read file: {}", path.display()))?;
let language_config = self
.registry
.detect_language_arc(path)
.with_context(|| format!("Unsupported file type: {}", path.display()))?;
let language_name = if language_config.name.bytes().all(|byte| !byte.is_ascii_uppercase()) {
Cow::Borrowed(language_config.name.as_str())
} else {
Cow::Owned(language_config.name.to_lowercase())
};
let mut resolved_config = config_manager.get_config_for_file_with_language(path, &language_name);
if let Some(overrides) = cli_overrides {
if overrides.remove_doc {
resolved_config.remove_docs = true;
}
if !overrides.use_default_ignores {
resolved_config.use_default_ignores = false;
}
if overrides.remove_todo {
resolved_config.remove_todos = true;
}
if overrides.remove_fixme {
resolved_config.remove_fixme = true;
}
if !overrides.custom_preserve_patterns.is_empty() {
resolved_config
.preserve_patterns
.extend(overrides.custom_preserve_patterns.iter().cloned());
}
if !overrides.respect_gitignore {
resolved_config.respect_gitignore = false;
}
if overrides.traverse_git_repos {
resolved_config.traverse_git_repos = true;
}
}
let outcome = self.process_content_with_config(&content, language_config.as_ref(), &resolved_config)?;
Ok(ProcessedFile {
path: path.to_path_buf(),
original_content: content,
processed_content: outcome.content,
modified: false,
comments_removed: outcome.removed_comments.len(),
removed_comments: outcome.removed_comments,
removed_ranges: outcome.removed_ranges,
important_removals: outcome.important_removals,
redundant_markers: outcome.redundant_markers,
})
}
fn process_content_with_config(
&mut self,
content: &str,
language_config: &crate::languages::config::LanguageConfig,
resolved_config: &ResolvedConfig,
) -> Result<ProcessOutcome> {
let language = tree_sitter_language_pack::get_language(&language_config.tslp_name).with_context(|| {
format!(
"Failed to load grammar for '{}' (tslp name: '{}')",
language_config.name, language_config.tslp_name
)
})?;
self.parser
.set_language(&language)
.context("Failed to set parser language")?;
let tree = self
.parser
.parse(content, None)
.context("Failed to parse source code")?;
let preservation_rules = self.create_preservation_rules_from_config(resolved_config);
let mut visitor = CommentVisitor::new_with_language(
content,
&preservation_rules,
&language_config.comment_types,
&language_config.doc_comment_types,
&language_config.name,
);
visitor.visit_node(tree.root_node());
visitor.extend_keep_blocks();
visitor.extend_keep_above();
let marker_ranges = visitor.redundant_keep_markers(resolved_config.remove_docs);
let redundant_markers = marker_ranges
.iter()
.map(|&(start, _)| RedundantMarker {
line: content[..start].bytes().filter(|&byte| byte == b'\n').count(),
preview: first_line_preview(line_containing(content, start)),
})
.collect();
let comments_to_remove = dedupe_nested(visitor.get_comments_to_remove());
let removed_comments = comments_to_remove
.iter()
.map(|comment| RemovedComment {
start_row: comment.start_row,
end_row: comment.end_row,
is_documentation: comment.is_documentation,
preview: first_line_preview(comment.content(content)),
})
.collect();
let important_removals = detect_important_removals(&comments_to_remove, content);
let (output, removed_ranges) = self.remove_comments_from_content(content, &comments_to_remove, &marker_ranges);
Ok(ProcessOutcome {
content: output,
removed_comments,
important_removals,
removed_ranges,
redundant_markers,
})
}
fn create_preservation_rules_from_config(&self, config: &ResolvedConfig) -> Vec<PreservationRule> {
let mut rules = Vec::new();
rules.push(PreservationRule::shebang());
rules.push(PreservationRule::pattern("~keep"));
if !config.remove_todos {
rules.push(PreservationRule::pattern("TODO"));
rules.push(PreservationRule::pattern("todo"));
}
if !config.remove_fixme {
rules.push(PreservationRule::pattern("FIXME"));
rules.push(PreservationRule::pattern("fixme"));
}
if !config.remove_docs {
rules.push(PreservationRule::documentation());
}
for pattern in &config.preserve_patterns {
rules.push(PreservationRule::pattern_owned(pattern.clone()));
}
if config.use_default_ignores {
let mut comprehensive_rules = PreservationRule::comprehensive_rules();
if config.remove_todos {
comprehensive_rules.retain(|rule| !rule.pattern_matches("TODO") && !rule.pattern_matches("todo"));
}
if config.remove_fixme {
comprehensive_rules.retain(|rule| !rule.pattern_matches("FIXME") && !rule.pattern_matches("fixme"));
}
if config.remove_docs {
comprehensive_rules.retain(|rule| !matches!(rule, PreservationRule::Documentation));
}
rules.extend(comprehensive_rules);
}
rules
}
fn remove_comments_from_content(
&self,
content: &str,
comments_to_remove: &[&CommentInfo],
marker_ranges: &[(usize, usize)],
) -> (String, Vec<(usize, usize)>) {
if comments_to_remove.is_empty() && marker_ranges.is_empty() {
return (content.to_string(), Vec::new());
}
let bytes = content.as_bytes();
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(comments_to_remove.len());
for comment in comments_to_remove {
ranges.push((comment.start_byte, comment.end_byte));
}
ranges.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
let mut filtered: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
if let Some(previous) = filtered.last()
&& start >= previous.0
&& end <= previous.1
{
continue;
}
filtered.push((start, end));
}
let mut removal_ranges: Vec<(usize, usize)> = Vec::with_capacity(filtered.len() + marker_ranges.len());
for (start, end) in &filtered {
if let Some(range) = Self::expand_range(bytes, *start, *end) {
removal_ranges.push(range);
}
}
removal_ranges.extend_from_slice(marker_ranges);
removal_ranges.sort_unstable();
removal_ranges.dedup();
let mut output = String::with_capacity(content.len());
let mut cursor = 0;
for (start, end) in &removal_ranges {
let start = cursor.max(*start);
if cursor < start {
output.push_str(&content[cursor..start]);
}
cursor = *end;
}
if cursor < content.len() {
output.push_str(&content[cursor..]);
}
(output, removal_ranges)
}
fn expand_range(bytes: &[u8], start: usize, end: usize) -> Option<(usize, usize)> {
let end = end.min(bytes.len());
if start >= end || start >= bytes.len() {
return None;
}
let line_start = match memchr::memrchr(b'\n', &bytes[..start]) {
Some(pos) => pos + 1,
None => 0,
};
let line_end = match memchr::memchr(b'\n', &bytes[end..]) {
Some(pos) => end + pos + 1,
None => bytes.len(),
};
let before = &bytes[line_start..start];
let after = &bytes[end..line_end];
let before_ws = before.iter().all(|b| b.is_ascii_whitespace());
let after_ws = after.iter().all(|b| b.is_ascii_whitespace());
if before_ws && after_ws {
Some((line_start, line_end))
} else if after_ws {
Some((start - trailing_horizontal_whitespace(before), end))
} else {
Some((start, end))
}
}
pub fn plan_removals(&mut self, content: &str, path: &Path, config: &ResolvedConfig) -> Result<Vec<Removal>> {
let removals = self
.inspect(content, path, config)?
.into_iter()
.filter_map(|comment| match comment.verdict {
Verdict::Remove {
expanded_start,
expanded_end,
} => Some(Removal {
comment_start: comment.start_byte,
comment_end: comment.end_byte,
remove_start: expanded_start,
remove_end: expanded_end,
start_row: comment.start_row,
is_documentation: comment.is_documentation,
preview: first_line_preview(&comment.text),
}),
Verdict::Preserve => None,
})
.collect();
Ok(removals)
}
pub fn inspect(&mut self, content: &str, path: &Path, config: &ResolvedConfig) -> Result<Vec<InspectedComment>> {
let language_config = self
.registry
.detect_language_arc(path)
.with_context(|| format!("Unsupported file type: {}", path.display()))?;
let language = tree_sitter_language_pack::get_language(&language_config.tslp_name).with_context(|| {
format!(
"Failed to load grammar for '{}' (tslp name: '{}')",
language_config.name, language_config.tslp_name
)
})?;
self.parser
.set_language(&language)
.context("Failed to set parser language")?;
let tree = self
.parser
.parse(content, None)
.context("Failed to parse source code")?;
let preservation_rules = self.create_preservation_rules_from_config(config);
let mut visitor = CommentVisitor::new_with_language(
content,
&preservation_rules,
&language_config.comment_types,
&language_config.doc_comment_types,
&language_config.name,
);
visitor.visit_node(tree.root_node());
visitor.extend_keep_blocks();
visitor.extend_keep_above();
let comments = visitor.comments();
let mut selected = dedupe_nested_indices(comments, |comment| !comment.should_preserve);
selected.extend(dedupe_nested_indices(comments, |comment| comment.should_preserve));
selected.sort_by(|&a, &b| comments[a].start_byte.cmp(&comments[b].start_byte));
let bytes = content.as_bytes();
let syntax = language_config.resolve_comment_syntax();
let mut inspected = Vec::with_capacity(selected.len());
for index in selected {
let comment = &comments[index];
let text = comment.content(content);
let verdict = if comment.should_preserve {
Verdict::Preserve
} else {
match Self::expand_range(bytes, comment.start_byte, comment.end_byte) {
Some((expanded_start, expanded_end)) => Verdict::Remove {
expanded_start,
expanded_end,
},
None => continue,
}
};
let reason = if comment.should_preserve {
preserve_reason(visitor.decision(index), visitor.is_extended(index))
} else {
None
};
inspected.push(InspectedComment {
start_byte: comment.start_byte,
end_byte: comment.end_byte,
start_row: comment.start_row,
end_row: comment.end_row,
node_type: comment.node_type.clone(),
kind: classify_kind(comment, text, syntax),
verdict,
reason,
text: text.to_string(),
is_documentation: comment.is_documentation,
});
}
Ok(inspected)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommentKind {
Line,
Block,
Doc,
Docstring,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PreserveReason {
Pattern(String),
Documentation,
FileHeader,
Shebang,
KeepMarker,
ExtendedByNeighbourMarker,
LanguageDirective,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Remove {
expanded_start: usize,
expanded_end: usize,
},
Preserve,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InspectedComment {
pub start_byte: usize,
pub end_byte: usize,
pub start_row: usize,
pub end_row: usize,
pub node_type: String,
pub kind: CommentKind,
pub verdict: Verdict,
pub reason: Option<PreserveReason>,
pub text: String,
pub is_documentation: bool,
}
fn preserve_reason(decision: VisitDecision<'_>, extended: bool) -> Option<PreserveReason> {
if let Some(rule) = decision.matched_rule {
return Some(match rule {
PreservationRule::Pattern(pattern) if pattern.as_ref() == KEEP_MARKER => PreserveReason::KeepMarker,
PreservationRule::Pattern(pattern) => PreserveReason::Pattern(pattern.as_ref().to_string()),
PreservationRule::PatternCaseInsensitive(pattern) => PreserveReason::Pattern(pattern.as_ref().to_string()),
PreservationRule::Documentation => PreserveReason::Documentation,
PreservationRule::FileHeader => PreserveReason::FileHeader,
PreservationRule::Shebang => PreserveReason::Shebang,
});
}
if decision.forced_by_grammar {
return Some(PreserveReason::LanguageDirective);
}
extended.then_some(PreserveReason::ExtendedByNeighbourMarker)
}
fn classify_kind(comment: &CommentInfo, text: &str, syntax: CommentSyntaxResolution) -> CommentKind {
if comment.node_type.contains("string") {
return CommentKind::Docstring;
}
if CommentVisitor::is_doc_comment(comment, text) {
return CommentKind::Doc;
}
if let CommentSyntaxResolution::Resolved(syntax) = syntax {
let trimmed = text.trim_start();
if let Some((open, _)) = syntax.block
&& trimmed.starts_with(open)
{
return CommentKind::Block;
}
if let Some(line) = syntax.line
&& trimmed.starts_with(line)
{
return CommentKind::Line;
}
}
if comment.start_row == comment.end_row {
CommentKind::Line
} else {
CommentKind::Block
}
}
fn dedupe_nested_indices(comments: &[CommentInfo], include: impl Fn(&CommentInfo) -> bool) -> Vec<usize> {
let mut indices: Vec<usize> = (0..comments.len()).filter(|&index| include(&comments[index])).collect();
indices.sort_by(|&a, &b| {
comments[a]
.start_byte
.cmp(&comments[b].start_byte)
.then(comments[b].end_byte.cmp(&comments[a].end_byte))
});
let mut kept: Vec<usize> = Vec::with_capacity(indices.len());
for index in indices {
let nested = kept.last().is_some_and(|&outer| {
comments[index].start_byte >= comments[outer].start_byte
&& comments[index].end_byte <= comments[outer].end_byte
});
if !nested {
kept.push(index);
}
}
kept
}
const KEEP_MARKER: &str = "~keep";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Removal {
pub comment_start: usize,
pub comment_end: usize,
pub remove_start: usize,
pub remove_end: usize,
pub start_row: usize,
pub is_documentation: bool,
pub preview: String,
}
const PREVIEW_MAX_CHARS: usize = 80;
struct ProcessOutcome {
content: String,
removed_comments: Vec<RemovedComment>,
important_removals: Vec<ImportantRemoval>,
removed_ranges: Vec<(usize, usize)>,
redundant_markers: Vec<RedundantMarker>,
}
#[derive(Debug)]
pub struct ProcessedFile {
pub path: std::path::PathBuf,
pub original_content: String,
pub processed_content: String,
pub modified: bool,
pub comments_removed: usize,
pub removed_comments: Vec<RemovedComment>,
pub removed_ranges: Vec<(usize, usize)>,
pub important_removals: Vec<ImportantRemoval>,
pub redundant_markers: Vec<RedundantMarker>,
}
#[derive(Debug, Clone)]
pub struct RemovedComment {
pub start_row: usize,
pub end_row: usize,
pub is_documentation: bool,
pub preview: String,
}
#[derive(Debug, Clone)]
pub struct RedundantMarker {
pub line: usize,
pub preview: String,
}
#[derive(Debug, Clone)]
pub struct ImportantRemoval {
pub line: usize,
pub reason: Cow<'static, str>,
pub preview: String,
}
fn dedupe_nested(comments: Vec<&CommentInfo>) -> Vec<&CommentInfo> {
let mut comments = comments;
comments.sort_by(|a, b| a.start_byte.cmp(&b.start_byte).then(b.end_byte.cmp(&a.end_byte)));
let mut kept: Vec<&CommentInfo> = Vec::with_capacity(comments.len());
for comment in comments {
let nested = kept
.last()
.is_some_and(|outer| comment.start_byte >= outer.start_byte && comment.end_byte <= outer.end_byte);
if !nested {
kept.push(comment);
}
}
kept
}
fn trailing_horizontal_whitespace(bytes: &[u8]) -> usize {
bytes
.iter()
.rev()
.take_while(|byte| matches!(byte, b' ' | b'\t'))
.count()
}
fn line_containing(content: &str, offset: usize) -> &str {
let start = content[..offset].rfind('\n').map_or(0, |pos| pos + 1);
let end = content[offset..].find('\n').map_or(content.len(), |pos| offset + pos);
&content[start..end]
}
fn first_line_preview(content: &str) -> String {
content
.lines()
.next()
.unwrap_or_default()
.trim()
.chars()
.take(PREVIEW_MAX_CHARS)
.collect()
}
pub struct OutputWriter {
dry_run: bool,
verbose: bool,
show_diff: bool,
quiet: bool,
}
impl OutputWriter {
pub fn new(dry_run: bool, verbose: bool, show_diff: bool, quiet: bool) -> Self {
Self {
dry_run,
verbose,
show_diff,
quiet,
}
}
pub fn write_file(&self, processed_file: &ProcessedFile) -> Result<()> {
use crate::ui;
let modified = processed_file.original_content != processed_file.processed_content;
if modified && !self.dry_run {
std::fs::write(&processed_file.path, &processed_file.processed_content)
.with_context(|| format!("Failed to write file: {}", processed_file.path.display()))?;
}
if self.quiet {
return Ok(());
}
if !modified {
if self.verbose {
anstream::println!(
"{} {} {}",
ui::success(ui::CHECK),
ui::dim("No changes needed:"),
ui::path(&processed_file.path)
);
}
return Ok(());
}
let count = processed_file.comments_removed;
let ranges = ui::format_line_ranges(
processed_file
.removed_comments
.iter()
.map(|comment| (comment.start_row, comment.end_row)),
self.verbose,
);
let mut parts: Vec<String> = Vec::new();
if count > 0 {
let verb = if self.dry_run { "would remove" } else { "removed" };
parts.push(format!("{verb} {count} ({ranges})"));
}
let markers = processed_file.redundant_markers.len();
if markers > 0 {
let verb = if self.dry_run { "would strip" } else { "stripped" };
let marker_lines = ui::format_line_ranges(
processed_file
.redundant_markers
.iter()
.map(|marker| (marker.line, marker.line)),
self.verbose,
);
let plural = if markers == 1 { "marker" } else { "markers" };
parts.push(format!("{verb} {markers} redundant ~keep {plural} ({marker_lines})"));
}
let detail = parts.join(", ");
if self.dry_run {
anstream::println!(
"{} {} {} {}",
ui::accent("[DRY RUN]"),
ui::dim("Would modify:"),
ui::path(&processed_file.path),
ui::dim(format!("— {detail}")),
);
} else {
anstream::println!(
"{} {} {}",
ui::success("Modified:"),
ui::path(&processed_file.path),
ui::dim(format!("— {detail}")),
);
}
if self.verbose {
for comment in &processed_file.removed_comments {
anstream::println!(
" {} {}",
ui::accent(ui::line_span(comment.start_row, comment.end_row)),
ui::dim(&comment.preview),
);
}
for marker in &processed_file.redundant_markers {
anstream::println!(
" {} {} {}",
ui::accent(ui::line_span(marker.line, marker.line)),
ui::dim("~keep stripped:"),
ui::dim(&marker.preview),
);
}
}
if self.show_diff {
self.show_diff(processed_file);
}
Ok(())
}
fn show_diff(&self, processed_file: &ProcessedFile) {
use crate::ui;
const CONTEXT: usize = 2;
let content = &processed_file.original_content;
let merged = merge_ranges(&processed_file.removed_ranges);
let mut records: Vec<DiffLine> = Vec::new();
let mut offset = 0usize;
for raw in content.split_inclusive('\n') {
let line_start = offset;
let line_full_end = offset + raw.len();
offset = line_full_end;
let text_end = line_full_end - usize::from(raw.ends_with('\n'));
let text = &content[line_start..text_end];
let remaining = cut_ranges(content, line_start, text_end, &merged);
let kind = if remaining == text {
DiffKind::Context
} else if remaining.trim().is_empty() {
DiffKind::Removed
} else {
DiffKind::Changed { remaining }
};
records.push(DiffLine {
text: text.to_string(),
kind,
});
}
let total = records.len();
let mut show = vec![false; total];
for (index, record) in records.iter().enumerate() {
if !matches!(record.kind, DiffKind::Context) {
let lo = index.saturating_sub(CONTEXT);
let hi = (index + CONTEXT).min(total.saturating_sub(1));
show.iter_mut().take(hi + 1).skip(lo).for_each(|flag| *flag = true);
}
}
anstream::println!();
anstream::println!("{}", ui::dim(format!("--- {}", processed_file.path.display())));
anstream::println!(
"{}",
ui::dim(format!("+++ {} (processed)", processed_file.path.display()))
);
let width = format!("{total}").len();
let mut printed_any = false;
for (index, record) in records.iter().enumerate() {
if !show[index] {
continue;
}
if printed_any && index > 0 && !show[index - 1] {
anstream::println!("{}", ui::dim(" ⋯"));
}
printed_any = true;
let number = format!("{:>width$}", index + 1, width = width);
match &record.kind {
DiffKind::Context => {
anstream::println!("{} {}", ui::dim(&number), ui::dim(format!(" {}", record.text)));
}
DiffKind::Removed => {
anstream::println!("{} {}", ui::dim(&number), ui::danger(format!("-{}", record.text)));
}
DiffKind::Changed { remaining } => {
anstream::println!("{} {}", ui::dim(&number), ui::danger(format!("-{}", record.text)));
anstream::println!("{} {}", ui::dim(&number), ui::success(format!("+{remaining}")));
}
}
}
}
pub fn print_summary(&self, total_files: usize, modified_files: usize, comments_removed: usize) {
crate::ui::print_summary(total_files, modified_files, comments_removed, self.dry_run);
}
}
struct DiffLine {
text: String,
kind: DiffKind,
}
enum DiffKind {
Context,
Removed,
Changed { remaining: String },
}
fn merge_ranges(ranges: &[(usize, usize)]) -> Vec<(usize, usize)> {
let mut sorted = ranges.to_vec();
sorted.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(sorted.len());
for (start, end) in sorted {
match merged.last_mut() {
Some(last) if start <= last.1 => last.1 = last.1.max(end),
_ => merged.push((start, end)),
}
}
merged
}
fn cut_ranges(content: &str, from: usize, to: usize, merged: &[(usize, usize)]) -> String {
let mut out = String::new();
let mut cursor = from;
for &(start, end) in merged {
if end <= from || start >= to {
continue;
}
let start = start.max(from);
let end = end.min(to);
if cursor < start {
out.push_str(&content[cursor..start]);
}
cursor = cursor.max(end);
}
if cursor < to {
out.push_str(&content[cursor..to]);
}
out
}
fn detect_important_removals(comments_to_remove: &[&CommentInfo], source: &str) -> Vec<ImportantRemoval> {
comments_to_remove
.iter()
.copied()
.filter_map(|comment| {
let trimmed = comment.content(source).trim_start();
let reason = if trimmed.starts_with("#!") {
Some(Cow::Borrowed("shebang"))
} else if trimmed.starts_with("//go:")
|| trimmed.starts_with("/*go:")
|| trimmed.starts_with("//+build")
|| trimmed.starts_with("// +build")
|| trimmed.starts_with("//line ")
|| trimmed.starts_with("/*line ")
{
Some(Cow::Borrowed("go directive"))
} else if trimmed.contains("shellcheck") {
Some(Cow::Borrowed("shellcheck directive"))
} else if trimmed.contains("eslint-")
|| trimmed.contains("prettier-")
|| trimmed.contains("@ts-")
|| trimmed.contains("biome-")
|| trimmed.contains("deno-")
|| trimmed.contains("nolint")
{
Some(Cow::Borrowed("linter/formatter directive"))
} else if trimmed.starts_with("#pragma") || trimmed.contains("NOLINT") || trimmed.contains("clang-format") {
Some(Cow::Borrowed("compiler/formatter directive"))
} else if trimmed.starts_with("# frozen_string_literal:")
|| trimmed.starts_with("# encoding:")
|| trimmed.starts_with("# coding:")
|| trimmed.starts_with("# typed:")
{
Some(Cow::Borrowed("language magic comment"))
} else {
None
}?;
let normalized_preview = if trimmed.contains('\n') {
Cow::Owned(trimmed.replace('\n', " "))
} else {
Cow::Borrowed(trimmed)
};
let mut preview = normalized_preview.into_owned();
const MAX: usize = 120;
if preview.len() > MAX {
let mut cut = MAX;
while cut > 0 && !preview.is_char_boundary(cut) {
cut -= 1;
}
preview.truncate(cut);
preview.push('…');
}
Some(ImportantRemoval {
line: comment.start_row + 1,
reason,
preview,
})
})
.collect()
}
#[cfg(test)]
mod tests;