use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixItHint {
pub id: u64,
pub file: String,
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
pub replacement: String,
pub description: String,
pub category: FixItCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FixItCategory {
MissingSemicolon,
MissingInclude,
NamespaceQualifier,
ConstCorrectness,
OverrideKeyword,
NullptrConversion,
AutoType,
BracesAroundStatement,
RedundantVoid,
DeprecatedApi,
Other,
}
impl fmt::Display for FixItCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingSemicolon => write!(f, "missing_semicolon"),
Self::MissingInclude => write!(f, "missing_include"),
Self::NamespaceQualifier => write!(f, "namespace_qualifier"),
Self::ConstCorrectness => write!(f, "const_correctness"),
Self::OverrideKeyword => write!(f, "override_keyword"),
Self::NullptrConversion => write!(f, "nullptr_conversion"),
Self::AutoType => write!(f, "auto_type"),
Self::BracesAroundStatement => write!(f, "braces"),
Self::RedundantVoid => write!(f, "redundant_void"),
Self::DeprecatedApi => write!(f, "deprecated_api"),
Self::Other => write!(f, "other"),
}
}
}
impl FixItHint {
pub fn new(
id: u64,
file: &str,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
replacement: &str,
description: &str,
category: FixItCategory,
) -> Self {
Self {
id,
file: file.to_string(),
start_line,
start_col,
end_line,
end_col,
replacement: replacement.to_string(),
description: description.to_string(),
category,
}
}
}
#[derive(Debug, Clone)]
pub struct FixItApplicator {
pub applied: Vec<FixItHint>,
pub rejected: Vec<(FixItHint, String)>,
}
impl FixItApplicator {
pub fn new() -> Self {
Self {
applied: Vec::new(),
rejected: Vec::new(),
}
}
pub fn apply_one(&mut self, fixit: &FixItHint, source: &str) -> Result<String, String> {
let lines: Vec<&str> = source.lines().collect();
if fixit.start_line == 0 || fixit.start_line > lines.len() {
self.rejected
.push((fixit.clone(), "invalid line range".into()));
return Err("invalid line range".into());
}
if fixit.start_line == fixit.end_line {
let line = lines[fixit.start_line - 1];
if fixit.start_col > line.len() + 1 {
self.rejected.push((fixit.clone(), "invalid column".into()));
return Err("invalid column".into());
}
let before = &line[..fixit.start_col - 1];
let after = &line[fixit.end_col - 1..];
let new_line = format!("{}{}{}", before, fixit.replacement, after);
let mut result = String::new();
for (i, l) in lines.iter().enumerate() {
if i + 1 == fixit.start_line {
result.push_str(&new_line);
} else {
result.push_str(l);
}
if i + 1 < lines.len() {
result.push('\n');
}
}
self.applied.push(fixit.clone());
Ok(result)
} else {
self.rejected
.push((fixit.clone(), "multi-line fix-its not yet supported".into()));
Err("multi-line not supported".into())
}
}
pub fn apply_all(&mut self, fixits: &[FixItHint], source: &str) -> String {
let mut current = source.to_string();
let sorted: Vec<&FixItHint> = {
let mut v: Vec<&FixItHint> = fixits.iter().collect();
v.sort_by_key(|f| (f.start_line, f.start_col));
v.reverse(); v
};
for fixit in &sorted {
if let Ok(new_source) = self.apply_one(fixit, ¤t) {
current = new_source;
}
}
current
}
pub fn applied_count(&self) -> usize {
self.applied.len()
}
pub fn rejected_count(&self) -> usize {
self.rejected.len()
}
}
impl Default for FixItApplicator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MultiFixIt {
pub hints: Vec<FixItHint>,
pub conflicts: Vec<(FixItHint, FixItHint, String)>,
}
impl MultiFixIt {
pub fn new() -> Self {
Self {
hints: Vec::new(),
conflicts: Vec::new(),
}
}
pub fn add_hint(&mut self, hint: FixItHint) {
self.hints.push(hint);
}
pub fn detect_conflicts(&mut self) {
self.conflicts.clear();
for i in 0..self.hints.len() {
for j in i + 1..self.hints.len() {
let a = &self.hints[i];
let b = &self.hints[j];
if a.file != b.file {
continue;
}
if a.start_line == b.start_line && a.start_col == b.start_col {
self.conflicts
.push((a.clone(), b.clone(), "same location".into()));
}
if a.start_line == b.start_line
&& a.start_col < b.end_col
&& b.start_col < a.end_col
{
self.conflicts
.push((a.clone(), b.clone(), "overlapping ranges".into()));
}
}
}
}
pub fn has_conflicts(&self) -> bool {
!self.conflicts.is_empty()
}
pub fn resolve_conflicts(&mut self) -> Vec<FixItHint> {
self.detect_conflicts();
let conflict_ids: HashSet<u64> = self
.conflicts
.iter()
.flat_map(|(a, b, _)| vec![a.id, b.id])
.collect();
self.hints
.iter()
.filter(|h| !conflict_ids.contains(&h.id))
.cloned()
.collect()
}
}
impl Default for MultiFixIt {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FixItRewriter {
pub source_file: String,
pub original: String,
pub modified: String,
pub changes: Vec<RewriteChange>,
}
#[derive(Debug, Clone)]
pub struct RewriteChange {
pub fixit_id: u64,
pub offset: usize,
pub length: usize,
pub replacement: String,
}
impl FixItRewriter {
pub fn new(source_file: &str, original: &str) -> Self {
Self {
source_file: source_file.to_string(),
original: original.to_string(),
modified: original.to_string(),
changes: Vec::new(),
}
}
pub fn apply_fixit(&mut self, fixit: &FixItHint) -> bool {
let applicator = FixItApplicator::new();
let mut app = applicator;
if let Ok(new_source) = app.apply_one(fixit, &self.modified) {
let change = RewriteChange {
fixit_id: fixit.id,
offset: 0, length: fixit.replacement.len(),
replacement: fixit.replacement.clone(),
};
self.changes.push(change);
self.modified = new_source;
true
} else {
false
}
}
pub fn apply_all(&mut self, fixits: &[FixItHint]) -> usize {
let applicator = FixItApplicator::new();
let mut app = applicator;
self.modified = app.apply_all(fixits, &self.original);
self.changes = app
.applied
.iter()
.map(|f| RewriteChange {
fixit_id: f.id,
offset: 0,
length: f.replacement.len(),
replacement: f.replacement.clone(),
})
.collect();
self.changes.len()
}
pub fn changes_count(&self) -> usize {
self.changes.len()
}
}
#[derive(Debug, Clone)]
pub struct FixItPreview {
pub file: String,
pub diffs: Vec<UnifiedDiffHunk>,
}
#[derive(Debug, Clone)]
pub struct UnifiedDiffHunk {
pub old_start: usize,
pub new_start: usize,
pub lines: Vec<DiffLine>,
}
#[derive(Debug, Clone)]
pub enum DiffLine {
Context(String),
Addition(String),
Removal(String),
}
impl FixItPreview {
pub fn new(file: &str) -> Self {
Self {
file: file.to_string(),
diffs: Vec::new(),
}
}
pub fn compute_diff(&mut self, original: &str, modified: &str) {
let orig_lines: Vec<&str> = original.lines().collect();
let mod_lines: Vec<&str> = modified.lines().collect();
let mut hunk = UnifiedDiffHunk {
old_start: 1,
new_start: 1,
lines: Vec::new(),
};
let max_len = orig_lines.len().max(mod_lines.len());
for i in 0..max_len {
let orig = orig_lines.get(i).copied().unwrap_or("");
let modf = mod_lines.get(i).copied().unwrap_or("");
if orig == modf {
hunk.lines.push(DiffLine::Context(orig.to_string()));
} else {
if orig.len() < mod_lines.len() || orig != modf {
hunk.lines.push(DiffLine::Removal(orig.to_string()));
hunk.lines.push(DiffLine::Addition(modf.to_string()));
}
}
}
self.diffs.push(hunk);
}
pub fn render(&self) -> String {
let mut out = String::new();
out.push_str(&format!("--- a/{}\n+++ b/{}\n", self.file, self.file));
for hunk in &self.diffs {
out.push_str(&format!(
"@@ -{},{} +{},{} @@\n",
hunk.old_start,
hunk.lines.len(),
hunk.new_start,
hunk.lines.len()
));
for line in &hunk.lines {
match line {
DiffLine::Context(s) => {
out.push_str(&format!(" {}\n", s));
}
DiffLine::Addition(s) => {
out.push_str(&format!("+{}\n", s));
}
DiffLine::Removal(s) => {
out.push_str(&format!("-{}\n", s));
}
}
}
}
out
}
}
#[derive(Debug, Clone)]
pub struct AutomatedFixIt {
pub next_id: u64,
}
impl AutomatedFixIt {
pub fn new() -> Self {
Self { next_id: 1 }
}
fn next_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id += 1;
id
}
pub fn suggest_missing_semicolons(&mut self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if !trimmed.is_empty()
&& !trimmed.ends_with(';')
&& !trimmed.ends_with('{')
&& !trimmed.ends_with('}')
&& !trimmed.ends_with(':')
&& !trimmed.starts_with('#')
&& !trimmed.starts_with("//")
&& !trimmed.starts_with("/*")
&& (trimmed.contains('=')
|| trimmed.contains("return")
|| trimmed.contains("int ")
|| trimmed.contains("auto "))
{
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
i + 1,
trimmed.len() + 1,
i + 1,
trimmed.len() + 1,
";",
"add missing semicolon",
FixItCategory::MissingSemicolon,
));
}
}
hints
}
pub fn suggest_includes(&mut self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
if source.contains("std::cout") && !source.contains("#include <iostream>") {
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
1,
1,
1,
1,
"#include <iostream>\n",
"add missing iostream include",
FixItCategory::MissingInclude,
));
}
if source.contains("std::vector") && !source.contains("#include <vector>") {
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
1,
1,
1,
1,
"#include <vector>\n",
"add missing vector include",
FixItCategory::MissingInclude,
));
}
if source.contains("std::string") && !source.contains("#include <string>") {
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
1,
1,
1,
1,
"#include <string>\n",
"add missing string include",
FixItCategory::MissingInclude,
));
}
hints
}
pub fn suggest_override(&mut self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("virtual")
&& line.contains(')')
&& !line.contains("override")
&& !line.contains("= 0")
{
if let Some(close_paren) = line.rfind(')') {
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
i + 1,
close_paren + 2,
i + 1,
close_paren + 2,
" override",
"add override specifier",
FixItCategory::OverrideKeyword,
));
}
}
}
hints
}
pub fn suggest_nullptr(&mut self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("NULL") {
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
i + 1,
line.find("NULL").unwrap() + 1,
i + 1,
line.find("NULL").unwrap() + 5,
"nullptr",
"replace NULL with nullptr",
FixItCategory::NullptrConversion,
));
}
if line.contains("= 0")
&& (line.contains('*') || line.contains("char") || line.contains("int"))
&& !line.contains("= 0;")
{
hints.push(FixItHint::new(
self.next_id(),
"input.cpp",
i + 1,
line.find("= 0").unwrap() + 1,
i + 1,
line.find("= 0").unwrap() + 4,
"= nullptr",
"replace 0 with nullptr",
FixItCategory::NullptrConversion,
));
}
}
hints
}
pub fn suggest_all(&mut self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
hints.extend(self.suggest_missing_semicolons(source));
hints.extend(self.suggest_includes(source));
hints.extend(self.suggest_override(source));
hints.extend(self.suggest_nullptr(source));
hints
}
}
impl Default for AutomatedFixIt {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FixItVerifier {
pub verified: Vec<FixItHint>,
pub failed: Vec<(FixItHint, String)>,
}
impl FixItVerifier {
pub fn new() -> Self {
Self {
verified: Vec::new(),
failed: Vec::new(),
}
}
pub fn verify(&mut self, fixit: &FixItHint, _source: &str, _modified: &str) -> bool {
if fixit.replacement.is_empty() && fixit.end_col == fixit.start_col {
self.failed
.push((fixit.clone(), "empty replacement".into()));
false
} else {
self.verified.push(fixit.clone());
true
}
}
pub fn verify_all(&mut self, fixits: &[FixItHint], source: &str, modified: &str) -> usize {
let mut count = 0;
for fixit in fixits {
if self.verify(fixit, source, modified) {
count += 1;
}
}
count
}
pub fn verified_count(&self) -> usize {
self.verified.len()
}
pub fn failed_count(&self) -> usize {
self.failed.len()
}
}
impl Default for FixItVerifier {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FixItDatabase {
pub hints: HashMap<String, Vec<FixItHint>>,
}
impl FixItDatabase {
pub fn new() -> Self {
Self {
hints: HashMap::new(),
}
}
pub fn store(&mut self, file: &str, fixits: Vec<FixItHint>) {
self.hints.insert(file.to_string(), fixits);
}
pub fn load(&self, file: &str) -> Option<&Vec<FixItHint>> {
self.hints.get(file)
}
pub fn remove(&mut self, file: &str) {
self.hints.remove(file);
}
pub fn file_count(&self) -> usize {
self.hints.len()
}
pub fn total_hints(&self) -> usize {
self.hints.values().map(|v| v.len()).sum()
}
pub fn all_files(&self) -> Vec<&str> {
self.hints.keys().map(|s| s.as_str()).collect()
}
pub fn apply_batch(&self, sources: &HashMap<String, String>) -> HashMap<String, String> {
let mut results = HashMap::new();
for (file, source) in sources {
let mut applicator = FixItApplicator::new();
let mut app = applicator;
if let Some(fixits) = self.hints.get(file) {
let modified = app.apply_all(fixits, source);
results.insert(file.clone(), modified);
} else {
results.insert(file.clone(), source.clone());
}
}
results
}
}
impl Default for FixItDatabase {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClangTidyCheck {
ModernizeUseNullptr,
ModernizeUseOverride,
ModernizeUseAuto,
ModernizeLoopConvert,
ModernizeMakeUnique,
ModernizeMakeShared,
ReadabilityBracesAroundStmts,
ReadabilityContainerSizeEmpty,
ReadabilityRedundantStringCstr,
ReadabilityImplicitBoolConversion,
PerformanceUnnecessaryCopyInit,
PerformanceMoveConstArg,
PerformanceForRangeCopy,
CppCoreGuidelinesProTypeConstCast,
CppCoreGuidelinesAvoidGoto,
BugproneIntegerDivision,
BugproneSuspiciousMemset,
BugproneMisplacedWideningCast,
MiscUnusedParameters,
MiscNewDeleteOverloads,
}
impl ClangTidyCheck {
pub fn check_name(&self) -> &'static str {
match self {
Self::ModernizeUseNullptr => "modernize-use-nullptr",
Self::ModernizeUseOverride => "modernize-use-override",
Self::ModernizeUseAuto => "modernize-use-auto",
Self::ModernizeLoopConvert => "modernize-loop-convert",
Self::ModernizeMakeUnique => "modernize-make-unique",
Self::ModernizeMakeShared => "modernize-make-shared",
Self::ReadabilityBracesAroundStmts => "readability-braces-around-statements",
Self::ReadabilityContainerSizeEmpty => "readability-container-size-empty",
Self::ReadabilityRedundantStringCstr => "readability-redundant-string-cstr",
Self::ReadabilityImplicitBoolConversion => "readability-implicit-bool-conversion",
Self::PerformanceUnnecessaryCopyInit => "performance-unnecessary-copy-initialization",
Self::PerformanceMoveConstArg => "performance-move-const-arg",
Self::PerformanceForRangeCopy => "performance-for-range-copy",
Self::CppCoreGuidelinesProTypeConstCast => "cppcoreguidelines-pro-type-const-cast",
Self::CppCoreGuidelinesAvoidGoto => "cppcoreguidelines-avoid-goto",
Self::BugproneIntegerDivision => "bugprone-integer-division",
Self::BugproneSuspiciousMemset => "bugprone-suspicious-memset",
Self::BugproneMisplacedWideningCast => "bugprone-misplaced-widening-cast",
Self::MiscUnusedParameters => "misc-unused-parameters",
Self::MiscNewDeleteOverloads => "misc-new-delete-overloads",
}
}
pub fn category(&self) -> &'static str {
match self {
Self::ModernizeUseNullptr
| Self::ModernizeUseOverride
| Self::ModernizeUseAuto
| Self::ModernizeLoopConvert
| Self::ModernizeMakeUnique
| Self::ModernizeMakeShared => "modernize",
Self::ReadabilityBracesAroundStmts
| Self::ReadabilityContainerSizeEmpty
| Self::ReadabilityRedundantStringCstr
| Self::ReadabilityImplicitBoolConversion => "readability",
Self::PerformanceUnnecessaryCopyInit
| Self::PerformanceMoveConstArg
| Self::PerformanceForRangeCopy => "performance",
Self::CppCoreGuidelinesProTypeConstCast | Self::CppCoreGuidelinesAvoidGoto => {
"cppcoreguidelines"
}
Self::BugproneIntegerDivision
| Self::BugproneSuspiciousMemset
| Self::BugproneMisplacedWideningCast => "bugprone",
Self::MiscUnusedParameters | Self::MiscNewDeleteOverloads => "misc",
}
}
}
#[derive(Debug, Clone)]
pub struct ClangTidyIntegration {
pub enabled_checks: HashSet<ClangTidyCheck>,
pub fixit_db: FixItDatabase,
}
impl ClangTidyIntegration {
pub fn new() -> Self {
let mut enabled = HashSet::new();
for check in &[
ClangTidyCheck::ModernizeUseNullptr,
ClangTidyCheck::ModernizeUseOverride,
ClangTidyCheck::ModernizeUseAuto,
ClangTidyCheck::ModernizeLoopConvert,
ClangTidyCheck::ReadabilityBracesAroundStmts,
ClangTidyCheck::PerformanceUnnecessaryCopyInit,
ClangTidyCheck::PerformanceForRangeCopy,
] {
enabled.insert(*check);
}
Self {
enabled_checks: enabled,
fixit_db: FixItDatabase::new(),
}
}
pub fn enable_check(&mut self, check: ClangTidyCheck) {
self.enabled_checks.insert(check);
}
pub fn disable_check(&mut self, check: ClangTidyCheck) {
self.enabled_checks.remove(&check);
}
pub fn run_checks(&self, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
for check in &self.enabled_checks {
let check_hints = self.generate_fixits(*check, source);
hints.extend(check_hints);
}
hints
}
fn generate_fixits(&self, check: ClangTidyCheck, source: &str) -> Vec<FixItHint> {
let mut hints = Vec::new();
let lines: Vec<&str> = source.lines().collect();
match check {
ClangTidyCheck::ModernizeUseNullptr => {
for (i, line) in lines.iter().enumerate() {
if line.contains("NULL") {
hints.push(FixItHint::new(
i as u64,
"input.cpp",
i + 1,
line.find("NULL").unwrap_or(0) + 1,
i + 1,
line.find("NULL").unwrap_or(0) + 5,
"nullptr",
"use nullptr (modernize-use-nullptr)",
FixItCategory::NullptrConversion,
));
}
}
}
ClangTidyCheck::ModernizeUseOverride => {
for (i, line) in lines.iter().enumerate() {
if line.contains("virtual")
&& !line.contains("override")
&& !line.contains("= 0")
{
if let Some(rp) = line.rfind(')') {
hints.push(FixItHint::new(
i as u64,
"input.cpp",
i + 1,
rp + 2,
i + 1,
rp + 2,
" override",
"add override (modernize-use-override)",
FixItCategory::OverrideKeyword,
));
}
}
}
}
ClangTidyCheck::ReadabilityBracesAroundStmts => {
for (i, line) in lines.iter().enumerate() {
let t = line.trim();
if (t.starts_with("if (") || t.starts_with("for (") || t.starts_with("while ("))
&& !t.ends_with('{')
&& i + 1 < lines.len()
&& lines[i + 1].trim() != "{"
{
hints.push(FixItHint::new(
i as u64,
"input.cpp",
i + 1,
t.len() + 1,
i + 1,
t.len() + 1,
" {",
"add braces (readability-braces-around-statements)",
FixItCategory::BracesAroundStatement,
));
}
}
}
ClangTidyCheck::PerformanceUnnecessaryCopyInit => {
for (i, line) in lines.iter().enumerate() {
if line.contains("auto ")
&& line.contains(" = ")
&& !line.contains('&')
&& !line.contains("std::move")
{
hints.push(FixItHint::new(
i as u64,
"input.cpp",
i + 1,
1,
i + 1,
6,
"const auto&",
"use const reference (performance-unnecessary-copy-initialization)",
FixItCategory::ConstCorrectness,
));
}
}
}
_ => {}
}
hints
}
pub fn apply_batch(&mut self, file: &str, source: &str) -> String {
let hints = self.run_checks(source);
let mut applicator = FixItApplicator::new();
let mut app = applicator;
let modified = app.apply_all(&hints, source);
self.fixit_db.store(file, hints);
modified
}
pub fn apply_batch_multi(
&mut self,
sources: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut results = HashMap::new();
for (file, source) in sources {
let modified = self.apply_batch(file, source);
results.insert(file.clone(), modified);
}
results
}
}
impl Default for ClangTidyIntegration {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixit_hint_creation() {
let f = FixItHint::new(
1,
"test.cpp",
1,
1,
1,
5,
"fix",
"desc",
FixItCategory::Other,
);
assert_eq!(f.id, 1);
assert_eq!(f.replacement, "fix");
}
#[test]
fn test_fixit_category_display() {
assert_eq!(
FixItCategory::MissingSemicolon.to_string(),
"missing_semicolon"
);
assert_eq!(
FixItCategory::NullptrConversion.to_string(),
"nullptr_conversion"
);
}
#[test]
fn test_applicator_apply_one() {
let mut app = FixItApplicator::new();
let fixit = FixItHint::new(
1,
"test.cpp",
1,
1,
1,
5,
"hello",
"test",
FixItCategory::Other,
);
let result = app.apply_one(&fixit, "world");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "hello");
}
#[test]
fn test_applicator_apply_invalid_line() {
let mut app = FixItApplicator::new();
let fixit = FixItHint::new(
1,
"test.cpp",
10,
1,
10,
5,
"x",
"bad",
FixItCategory::Other,
);
let result = app.apply_one(&fixit, "one line");
assert!(result.is_err());
}
#[test]
fn test_applicator_apply_all() {
let mut app = FixItApplicator::new();
let fixits = vec![FixItHint::new(
1,
"test.cpp",
1,
1,
1,
2,
"H",
"capitalize",
FixItCategory::Other,
)];
let result = app.apply_all(&fixits, "hello");
assert!(result.contains("H"));
}
#[test]
fn test_multi_fixit_conflict_detection() {
let mut mf = MultiFixIt::new();
mf.add_hint(FixItHint::new(
1,
"test.cpp",
5,
10,
5,
15,
"a",
"d1",
FixItCategory::Other,
));
mf.add_hint(FixItHint::new(
2,
"test.cpp",
5,
12,
5,
18,
"b",
"d2",
FixItCategory::Other,
));
mf.detect_conflicts();
assert!(mf.has_conflicts());
}
#[test]
fn test_multi_fixit_no_conflict() {
let mut mf = MultiFixIt::new();
mf.add_hint(FixItHint::new(
1,
"test.cpp",
1,
1,
1,
5,
"a",
"d1",
FixItCategory::Other,
));
mf.add_hint(FixItHint::new(
2,
"test.cpp",
10,
1,
10,
5,
"b",
"d2",
FixItCategory::Other,
));
mf.detect_conflicts();
assert!(!mf.has_conflicts());
}
#[test]
fn test_multi_fixit_resolve_conflicts() {
let mut mf = MultiFixIt::new();
mf.add_hint(FixItHint::new(
1,
"test.cpp",
5,
10,
5,
15,
"a",
"d1",
FixItCategory::Other,
));
mf.add_hint(FixItHint::new(
2,
"test.cpp",
5,
12,
5,
18,
"b",
"d2",
FixItCategory::Other,
));
mf.add_hint(FixItHint::new(
3,
"test.cpp",
10,
1,
10,
5,
"c",
"d3",
FixItCategory::Other,
));
let resolved = mf.resolve_conflicts();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].id, 3);
}
#[test]
fn test_rewriter_apply_fixit() {
let mut rw = FixItRewriter::new("test.cpp", "hello world");
let fixit = FixItHint::new(
1,
"test.cpp",
1,
1,
1,
6,
"HELLO",
"uppercase",
FixItCategory::Other,
);
assert!(rw.apply_fixit(&fixit));
assert_eq!(rw.changes_count(), 1);
}
#[test]
fn test_rewriter_apply_all() {
let mut rw = FixItRewriter::new("test.cpp", "abc");
let fixits = vec![
FixItHint::new(
1,
"test.cpp",
1,
1,
1,
2,
"A",
"cap_a",
FixItCategory::Other,
),
FixItHint::new(
2,
"test.cpp",
1,
2,
1,
3,
"B",
"cap_b",
FixItCategory::Other,
),
];
let count = rw.apply_all(&fixits);
assert_eq!(count, 2);
}
#[test]
fn test_preview_render() {
let mut preview = FixItPreview::new("test.cpp");
preview.compute_diff("old line", "new line");
let rendered = preview.render();
assert!(rendered.contains("--- a/test.cpp"));
assert!(rendered.contains("+++ b/test.cpp"));
}
#[test]
fn test_automated_fixit_missing_semicolons() {
let mut engine = AutomatedFixIt::new();
let hints = engine.suggest_missing_semicolons("int x = 5");
assert!(!hints.is_empty());
}
#[test]
fn test_automated_fixit_includes() {
let mut engine = AutomatedFixIt::new();
let hints = engine.suggest_includes("std::vector<int> v;");
assert!(!hints.is_empty());
}
#[test]
fn test_automated_fixit_override() {
let mut engine = AutomatedFixIt::new();
let hints = engine.suggest_override("virtual void foo() { }");
assert!(!hints.is_empty());
}
#[test]
fn test_automated_fixit_nullptr() {
let mut engine = AutomatedFixIt::new();
let hints = engine.suggest_nullptr("char* p = NULL;");
assert!(!hints.is_empty());
}
#[test]
fn test_automated_fixit_suggest_all() {
let mut engine = AutomatedFixIt::new();
let hints = engine.suggest_all("int x = 5\nchar* p = NULL;\nstd::cout << x;");
assert!(hints.len() >= 3);
}
#[test]
fn test_verifier_verify() {
let mut verifier = FixItVerifier::new();
let fixit = FixItHint::new(
1,
"test.cpp",
1,
1,
1,
5,
"hello",
"test",
FixItCategory::Other,
);
assert!(verifier.verify(&fixit, "world", "hello"));
assert_eq!(verifier.verified_count(), 1);
}
#[test]
fn test_verifier_empty_replacement() {
let mut verifier = FixItVerifier::new();
let fixit = FixItHint::new(1, "test.cpp", 1, 1, 1, 1, "", "empty", FixItCategory::Other);
assert!(!verifier.verify(&fixit, "src", "src"));
}
#[test]
fn test_database_store_load() {
let mut db = FixItDatabase::new();
let fixits = vec![FixItHint::new(
1,
"test.cpp",
1,
1,
1,
3,
"abc",
"d",
FixItCategory::Other,
)];
db.store("test.cpp", fixits);
assert!(db.load("test.cpp").is_some());
assert_eq!(db.total_hints(), 1);
}
#[test]
fn test_database_remove() {
let mut db = FixItDatabase::new();
db.store("test.cpp", vec![]);
assert_eq!(db.file_count(), 1);
db.remove("test.cpp");
assert_eq!(db.file_count(), 0);
}
#[test]
fn test_database_apply_batch() {
let mut db = FixItDatabase::new();
db.store(
"test.cpp",
vec![FixItHint::new(
1,
"test.cpp",
1,
1,
1,
1,
"// fixed\n",
"add comment",
FixItCategory::Other,
)],
);
let mut sources = HashMap::new();
sources.insert("test.cpp".to_string(), "original".to_string());
let results = db.apply_batch(&sources);
assert!(results.get("test.cpp").unwrap().contains("fixed"));
}
#[test]
fn test_clang_tidy_check_names() {
assert_eq!(
ClangTidyCheck::ModernizeUseNullptr.check_name(),
"modernize-use-nullptr"
);
assert_eq!(
ClangTidyCheck::ReadabilityBracesAroundStmts.check_name(),
"readability-braces-around-statements"
);
}
#[test]
fn test_clang_tidy_check_categories() {
assert_eq!(ClangTidyCheck::ModernizeUseOverride.category(), "modernize");
assert_eq!(
ClangTidyCheck::PerformanceUnnecessaryCopyInit.category(),
"performance"
);
}
#[test]
fn test_clang_tidy_integration() {
let integration = ClangTidyIntegration::new();
let hints = integration.run_checks("char* p = NULL;\nvirtual void f() { }");
assert!(!hints.is_empty());
}
#[test]
fn test_clang_tidy_apply_batch() {
let mut integration = ClangTidyIntegration::new();
let result = integration.apply_batch("test.cpp", "char* p = NULL;");
assert!(result.contains("nullptr"));
}
#[test]
fn test_clang_tidy_batch_multi() {
let mut integration = ClangTidyIntegration::new();
let mut sources = HashMap::new();
sources.insert("a.cpp".to_string(), "char* p = NULL;".to_string());
sources.insert("b.cpp".to_string(), "int x = NULL;".to_string());
let results = integration.apply_batch_multi(&sources);
assert_eq!(results.len(), 2);
assert!(results.get("a.cpp").unwrap().contains("nullptr"));
}
#[test]
fn test_clang_tidy_enable_disable() {
let mut integration = ClangTidyIntegration::new();
assert!(integration
.enabled_checks
.contains(&ClangTidyCheck::ModernizeUseNullptr));
integration.disable_check(ClangTidyCheck::ModernizeUseNullptr);
assert!(!integration
.enabled_checks
.contains(&ClangTidyCheck::ModernizeUseNullptr));
integration.enable_check(ClangTidyCheck::BugproneIntegerDivision);
assert!(integration
.enabled_checks
.contains(&ClangTidyCheck::BugproneIntegerDivision));
}
}