use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourcePosition {
pub line: usize,
pub column: usize,
}
impl SourcePosition {
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ToolingSourceRange {
pub start: SourcePosition,
pub end: SourcePosition,
}
impl ToolingSourceRange {
pub fn new(start: SourcePosition, end: SourcePosition) -> Self {
Self { start, end }
}
pub fn contains(&self, pos: SourcePosition) -> bool {
pos >= self.start && pos < self.end
}
pub fn overlaps(&self, other: &ToolingSourceRange) -> bool {
self.start < other.end && other.start < self.end
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone)]
pub struct ToolingReplacement {
pub file_path: String,
pub range: ToolingSourceRange,
pub replacement_text: String,
}
impl ToolingReplacement {
pub fn new(
file_path: &str,
range: ToolingSourceRange,
replacement_text: &str,
) -> Self {
Self {
file_path: file_path.to_string(),
range,
replacement_text: replacement_text.to_string(),
}
}
pub fn original_length(&self) -> usize {
if self.range.start.line == self.range.end.line {
self.range.end.column - self.range.start.column
} else {
(self.range.end.line - self.range.start.line) * 80
}
}
pub fn replacement_length(&self) -> usize {
self.replacement_text.len()
}
pub fn apply_to(&self, source: &str) -> String {
let lines: Vec<&str> = source.lines().collect();
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line_num < self.range.start.line {
result.push_str(line);
result.push('\n');
} else if line_num == self.range.start.line {
if self.range.start.line == self.range.end.line {
let prefix = &line[..self.range.start.column.min(line.len())];
let suffix = &line[self.range.end.column.min(line.len())..];
result.push_str(prefix);
result.push_str(&self.replacement_text);
result.push_str(suffix);
result.push_str("\n");
} else {
let prefix = &line[..self.range.start.column.min(line.len())];
result.push_str(prefix);
result.push_str(&self.replacement_text);
result.push('\n');
}
} else if line_num > self.range.start.line && line_num < self.range.end.line {
} else if line_num == self.range.end.line && self.range.start.line != self.range.end.line {
let suffix = &line[self.range.end.column.min(line.len())..];
result.push_str(suffix);
result.push('\n');
} else {
result.push_str(line);
result.push('\n');
}
}
result
}
}
pub struct ToolingSourceManager {
files: HashMap<String, String>,
file_paths: HashMap<String, PathBuf>,
pending_replacements: Vec<ToolingReplacement>,
}
impl ToolingSourceManager {
pub fn new() -> Self {
Self {
files: HashMap::new(),
file_paths: HashMap::new(),
pending_replacements: Vec::new(),
}
}
pub fn load_file(&mut self, path: &Path) -> Result<String, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Cannot read {}: {}", path.display(), e))?;
let key = path.to_string_lossy().to_string();
self.files.insert(key.clone(), content.clone());
self.file_paths.insert(key, path.to_path_buf());
Ok(content)
}
pub fn add_source(&mut self, name: &str, content: &str) {
self.files
.insert(name.to_string(), content.to_string());
}
pub fn get_source(&self, name: &str) -> Option<&str> {
self.files.get(name).map(|s| s.as_str())
}
pub fn file_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.files.keys().cloned().collect();
names.sort();
names
}
pub fn add_replacement(&mut self, replacement: ToolingReplacement) {
self.pending_replacements.push(replacement);
}
pub fn apply_replacements(&mut self) -> HashMap<String, String> {
let mut results: HashMap<String, String> = HashMap::new();
let mut file_replacements: HashMap<String, Vec<&ToolingReplacement>> = HashMap::new();
for r in &self.pending_replacements {
file_replacements
.entry(r.file_path.clone())
.or_default()
.push(r);
}
for (file, replacements) in &file_replacements {
if let Some(source) = self.files.get(file) {
let mut modified = source.clone();
let mut sorted: Vec<&&ToolingReplacement> = replacements.iter().collect();
sorted.sort_by_key(|r| (r.range.start.line, r.range.start.column));
sorted.reverse();
for r in &sorted {
modified = r.apply_to(&modified);
}
results.insert(file.clone(), modified);
}
}
self.pending_replacements.clear();
results
}
pub fn save_all(&mut self) -> Result<usize, String> {
let modified = self.apply_replacements();
let count = modified.len();
for (file, content) in &modified {
if let Some(path) = self.file_paths.get(file) {
fs::write(path, content)
.map_err(|e| format!("Cannot write {}: {}", path.display(), e))?;
}
}
Ok(count)
}
pub fn has_file(&self, name: &str) -> bool {
self.files.contains_key(name)
}
pub fn file_count(&self) -> usize {
self.files.len()
}
}
impl Default for ToolingSourceManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AstNodeKind {
FunctionDecl,
CXXMethodDecl,
CXXConstructorDecl,
CXXDestructorDecl,
VarDecl,
ParmVarDecl,
FieldDecl,
EnumDecl,
EnumConstantDecl,
RecordDecl,
CXXRecordDecl,
ClassTemplateDecl,
FunctionTemplateDecl,
NamespaceDecl,
UsingDecl,
TypedefDecl,
CallExpr,
CXXMemberCallExpr,
CXXOperatorCallExpr,
BinaryOperator,
UnaryOperator,
IfStmt,
ForStmt,
WhileStmt,
DoStmt,
SwitchStmt,
ReturnStmt,
CompoundStmt,
DeclStmt,
ExprWithCleanups,
ImplicitCastExpr,
CStyleCastExpr,
StaticCastExpr,
ReinterpretCastExpr,
ConstCastExpr,
DynamicCastExpr,
IntegerLiteral,
FloatingLiteral,
StringLiteral,
CharacterLiteral,
BooleanLiteral,
NullPtrLiteral,
}
impl AstNodeKind {
pub fn as_str(&self) -> &'static str {
match self {
AstNodeKind::FunctionDecl => "functionDecl",
AstNodeKind::CXXMethodDecl => "cxxMethodDecl",
AstNodeKind::CXXConstructorDecl => "cxxConstructorDecl",
AstNodeKind::CXXDestructorDecl => "cxxDestructorDecl",
AstNodeKind::VarDecl => "varDecl",
AstNodeKind::ParmVarDecl => "parmVarDecl",
AstNodeKind::FieldDecl => "fieldDecl",
AstNodeKind::EnumDecl => "enumDecl",
AstNodeKind::EnumConstantDecl => "enumConstantDecl",
AstNodeKind::RecordDecl => "recordDecl",
AstNodeKind::CXXRecordDecl => "cxxRecordDecl",
AstNodeKind::ClassTemplateDecl => "classTemplateDecl",
AstNodeKind::FunctionTemplateDecl => "functionTemplateDecl",
AstNodeKind::NamespaceDecl => "namespaceDecl",
AstNodeKind::UsingDecl => "usingDecl",
AstNodeKind::TypedefDecl => "typedefDecl",
AstNodeKind::CallExpr => "callExpr",
AstNodeKind::CXXMemberCallExpr => "cxxMemberCallExpr",
AstNodeKind::CXXOperatorCallExpr => "cxxOperatorCallExpr",
AstNodeKind::BinaryOperator => "binaryOperator",
AstNodeKind::UnaryOperator => "unaryOperator",
AstNodeKind::IfStmt => "ifStmt",
AstNodeKind::ForStmt => "forStmt",
AstNodeKind::WhileStmt => "whileStmt",
AstNodeKind::DoStmt => "doStmt",
AstNodeKind::SwitchStmt => "switchStmt",
AstNodeKind::ReturnStmt => "returnStmt",
AstNodeKind::CompoundStmt => "compoundStmt",
AstNodeKind::DeclStmt => "declStmt",
AstNodeKind::ExprWithCleanups => "exprWithCleanups",
AstNodeKind::ImplicitCastExpr => "implicitCastExpr",
AstNodeKind::CStyleCastExpr => "cStyleCastExpr",
AstNodeKind::StaticCastExpr => "staticCastExpr",
AstNodeKind::ReinterpretCastExpr => "reinterpretCastExpr",
AstNodeKind::ConstCastExpr => "constCastExpr",
AstNodeKind::DynamicCastExpr => "dynamicCastExpr",
AstNodeKind::IntegerLiteral => "integerLiteral",
AstNodeKind::FloatingLiteral => "floatingLiteral",
AstNodeKind::StringLiteral => "stringLiteral",
AstNodeKind::CharacterLiteral => "characterLiteral",
AstNodeKind::BooleanLiteral => "booleanLiteral",
AstNodeKind::NullPtrLiteral => "nullPtrLiteral",
}
}
}
#[derive(Debug, Clone)]
pub enum AstMatcher {
NodeKind(AstNodeKind),
HasName(String),
HasDescendant(Box<AstMatcher>),
HasAncestor(Box<AstMatcher>),
HasType(String),
AnyOf(Vec<AstMatcher>),
AllOf(Vec<AstMatcher>),
ParameterCountIs(usize),
Returns(String),
HasDeclaration(Box<AstMatcher>),
Unless(Box<AstMatcher>),
IsReferenced,
HasStringValue(String),
HasIntegerValue(i64),
Custom(String),
}
impl AstMatcher {
pub fn node_kind(kind: AstNodeKind) -> Self {
AstMatcher::NodeKind(kind)
}
pub fn has_name(name: &str) -> Self {
AstMatcher::HasName(name.to_string())
}
pub fn has_type(type_name: &str) -> Self {
AstMatcher::HasType(type_name.to_string())
}
pub fn any_of(matchers: Vec<AstMatcher>) -> Self {
AstMatcher::AnyOf(matchers)
}
pub fn all_of(matchers: Vec<AstMatcher>) -> Self {
AstMatcher::AllOf(matchers)
}
pub fn has_ancestor(matcher: AstMatcher) -> Self {
AstMatcher::HasAncestor(Box::new(matcher))
}
pub fn has_descendant(matcher: AstMatcher) -> Self {
AstMatcher::HasDescendant(Box::new(matcher))
}
pub fn unless(matcher: AstMatcher) -> Self {
AstMatcher::Unless(Box::new(matcher))
}
pub fn to_query_string(&self) -> String {
match self {
AstMatcher::NodeKind(kind) => kind.as_str().to_string(),
AstMatcher::HasName(name) => format!("hasName(\"{}\")", name),
AstMatcher::HasDescendant(inner) => {
format!("hasDescendant({})", inner.to_query_string())
}
AstMatcher::HasAncestor(inner) => {
format!("hasAncestor({})", inner.to_query_string())
}
AstMatcher::HasType(ty) => format!("hasType(\"{}\")", ty),
AstMatcher::AnyOf(matchers) => {
let inner: Vec<String> = matchers.iter().map(|m| m.to_query_string()).collect();
format!("anyOf({})", inner.join(", "))
}
AstMatcher::AllOf(matchers) => {
let inner: Vec<String> = matchers.iter().map(|m| m.to_query_string()).collect();
format!("allOf({})", inner.join(", "))
}
AstMatcher::ParameterCountIs(n) => format!("parameterCountIs({})", n),
AstMatcher::Returns(ty) => format!("returns({})", ty),
AstMatcher::HasDeclaration(inner) => {
format!("hasDeclaration({})", inner.to_query_string())
}
AstMatcher::Unless(inner) => format!("unless({})", inner.to_query_string()),
AstMatcher::IsReferenced => "isReferenced()".to_string(),
AstMatcher::HasStringValue(val) => format!("hasStringValue(\"{}\")", val),
AstMatcher::HasIntegerValue(val) => format!("hasIntegerValue({})", val),
AstMatcher::Custom(s) => s.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct AstMatchResult {
pub node_kind: AstNodeKind,
pub name: Option<String>,
pub source_range: Option<ToolingSourceRange>,
pub file_path: String,
pub bindings: HashMap<String, String>,
}
impl AstMatchResult {
pub fn new(kind: AstNodeKind, name: Option<&str>, file: &str) -> Self {
Self {
node_kind: kind,
name: name.map(|s| s.to_string()),
source_range: None,
file_path: file.to_string(),
bindings: HashMap::new(),
}
}
pub fn add_binding(&mut self, name: &str, value: &str) {
self.bindings.insert(name.to_string(), value.to_string());
}
pub fn get_binding(&self, name: &str) -> Option<&str> {
self.bindings.get(name).map(|s| s.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ClangCheckConfig {
pub source_files: Vec<String>,
pub include_dirs: Vec<String>,
pub defines: Vec<String>,
pub enable_checks: Vec<String>,
pub disable_checks: Vec<String>,
pub output_format: String,
}
impl Default for ClangCheckConfig {
fn default() -> Self {
Self {
source_files: Vec::new(),
include_dirs: Vec::new(),
defines: Vec::new(),
enable_checks: vec!["*".to_string()],
disable_checks: Vec::new(),
output_format: "text".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct ClangCheckResult {
pub file: String,
pub diagnostics: Vec<ClangCheckDiagnostic>,
pub passed: bool,
pub analysis_time_ms: u64,
}
#[derive(Debug, Clone)]
pub struct ClangCheckDiagnostic {
pub level: String,
pub message: String,
pub location: Option<SourcePosition>,
pub check_name: String,
pub fixits: Vec<ToolingReplacement>,
}
pub struct ClangCheck {
config: ClangCheckConfig,
}
impl ClangCheck {
pub fn new(config: ClangCheckConfig) -> Self {
Self { config }
}
pub fn run(&self) -> Vec<ClangCheckResult> {
let mut results = Vec::new();
for file in &self.config.source_files {
let result = self.analyze_file(file);
results.push(result);
}
results
}
pub fn analyze_file(&self, file: &str) -> ClangCheckResult {
let start = std::time::Instant::now();
let mut diagnostics = Vec::new();
if let Ok(content) = fs::read_to_string(file) {
self.check_line_length(&content, file, &mut diagnostics);
self.check_trailing_whitespace(&content, file, &mut diagnostics);
self.check_missing_newline(&content, file, &mut diagnostics);
self.check_tabs(&content, file, &mut diagnostics);
} else {
diagnostics.push(ClangCheckDiagnostic {
level: "error".to_string(),
message: format!("Cannot read file: {}", file),
location: None,
check_name: "file-access".to_string(),
fixits: Vec::new(),
});
}
let passed = !diagnostics.iter().any(|d| d.level == "error");
ClangCheckResult {
file: file.to_string(),
diagnostics,
passed,
analysis_time_ms: start.elapsed().as_millis() as u64,
}
}
fn check_line_length(&self, content: &str, file: &str, diags: &mut Vec<ClangCheckDiagnostic>) {
for (i, line) in content.lines().enumerate() {
if line.len() > 120 {
diags.push(ClangCheckDiagnostic {
level: "warning".to_string(),
message: format!(
"Line {} is too long ({} > 120 characters)",
i + 1,
line.len()
),
location: Some(SourcePosition::new(i + 1, 121)),
check_name: "line-length".to_string(),
fixits: Vec::new(),
});
}
}
}
fn check_trailing_whitespace(
&self,
content: &str,
file: &str,
diags: &mut Vec<ClangCheckDiagnostic>,
) {
for (i, line) in content.lines().enumerate() {
if line != line.trim_end() {
diags.push(ClangCheckDiagnostic {
level: "warning".to_string(),
message: format!("Line {} has trailing whitespace", i + 1),
location: Some(SourcePosition::new(i + 1, line.trim_end().len() + 1)),
check_name: "trailing-whitespace".to_string(),
fixits: vec![ToolingReplacement::new(
file,
ToolingSourceRange::new(
SourcePosition::new(i + 1, line.trim_end().len() + 1),
SourcePosition::new(i + 1, line.len() + 1),
),
"",
)],
});
}
}
}
fn check_missing_newline(
&self,
content: &str,
file: &str,
diags: &mut Vec<ClangCheckDiagnostic>,
) {
if !content.is_empty() && !content.ends_with('\n') {
let line_count = content.lines().count();
diags.push(ClangCheckDiagnostic {
level: "warning".to_string(),
message: "File does not end with a newline".to_string(),
location: Some(SourcePosition::new(line_count, 1)),
check_name: "missing-newline".to_string(),
fixits: Vec::new(),
});
}
}
fn check_tabs(&self, content: &str, file: &str, diags: &mut Vec<ClangCheckDiagnostic>) {
for (i, line) in content.lines().enumerate() {
if line.contains('\t') {
let col = line.find('\t').unwrap_or(0) + 1;
diags.push(ClangCheckDiagnostic {
level: "warning".to_string(),
message: format!("Line {} contains tab characters", i + 1),
location: Some(SourcePosition::new(i + 1, col)),
check_name: "tabs".to_string(),
fixits: Vec::new(),
});
}
}
}
pub fn print_results(&self, results: &[ClangCheckResult]) {
let mut total_diags = 0;
let mut errors = 0;
let mut warnings = 0;
for result in results {
println!("File: {}", result.file);
for diag in &result.diagnostics {
total_diags += 1;
let level = match diag.level.as_str() {
"error" => {
errors += 1;
"error"
}
"warning" => {
warnings += 1;
"warning"
}
_ => "note",
};
if let Some(loc) = &diag.location {
println!(
" {}:{}:{}: {}: {}",
result.file, loc.line, loc.column, level, diag.message
);
} else {
println!(" {}: {}: {}", result.file, level, diag.message);
}
}
}
println!(
"{} diagnostics: {} errors, {} warnings",
total_diags, errors, warnings
);
}
}
pub struct ClangQuery {
source_files: Vec<String>,
matchers: Vec<AstMatcher>,
results: Vec<AstMatchResult>,
}
impl ClangQuery {
pub fn new() -> Self {
Self {
source_files: Vec::new(),
matchers: Vec::new(),
results: Vec::new(),
}
}
pub fn add_file(&mut self, file: &str) {
self.source_files.push(file.to_string());
}
pub fn add_matcher(&mut self, matcher: AstMatcher) {
self.matchers.push(matcher);
}
pub fn set_matchers(&mut self, matchers: Vec<AstMatcher>) {
self.matchers = matchers;
}
pub fn run(&mut self) -> Vec<AstMatchResult> {
self.results.clear();
for file in &self.source_files.clone() {
if let Ok(content) = fs::read_to_string(file) {
let lines: Vec<&str> = content.lines().collect();
for matcher in &self.matchers.clone() {
self.match_in_text(matcher, file, &lines);
}
}
}
self.results.clone()
}
fn match_in_text(&mut self, matcher: &AstMatcher, file: &str, lines: &[&str]) {
match matcher {
AstMatcher::NodeKind(kind) => {
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if self.line_matches_node_kind(kind, trimmed) {
let name = self.extract_name(trimmed);
let mut result = AstMatchResult::new(kind.clone(), name.as_deref(), file);
result.source_range = Some(ToolingSourceRange::new(
SourcePosition::new(i + 1, 1),
SourcePosition::new(i + 1, trimmed.len() + 1),
));
self.results.push(result);
}
}
}
AstMatcher::HasName(name) => {
for (i, line) in lines.iter().enumerate() {
if line.contains(name.as_str()) {
let mut result = AstMatchResult::new(
AstNodeKind::FunctionDecl,
Some(name),
file,
);
result.source_range = Some(ToolingSourceRange::new(
SourcePosition::new(i + 1, 1),
SourcePosition::new(i + 1, line.len() + 1),
));
self.results.push(result);
}
}
}
_ => {
let mut result = AstMatchResult::new(
AstNodeKind::FunctionDecl,
None,
file,
);
result.add_binding("matcher", &matcher.to_query_string());
self.results.push(result);
}
}
}
fn line_matches_node_kind(&self, kind: &AstNodeKind, line: &str) -> bool {
match kind {
AstNodeKind::FunctionDecl => {
(line.contains('(') && line.contains(')') && !line.contains("if")
&& !line.contains("while") && !line.contains("for")
&& !line.contains("switch") && !line.starts_with("//"))
|| line.contains("void ") || line.contains("int ") || line.contains("char ")
}
AstNodeKind::VarDecl => {
(line.contains("int ") || line.contains("char ") || line.contains("float ")
|| line.contains("double ") || line.contains("auto "))
&& line.contains('=')
&& line.ends_with(';')
}
AstNodeKind::IfStmt => line.trim().starts_with("if"),
AstNodeKind::ForStmt => line.trim().starts_with("for"),
AstNodeKind::WhileStmt => line.trim().starts_with("while"),
AstNodeKind::ReturnStmt => line.trim().starts_with("return"),
AstNodeKind::CallExpr => line.contains('(') && line.contains(')'),
AstNodeKind::StringLiteral => line.contains('"'),
AstNodeKind::IntegerLiteral => {
line.chars().any(|c| c.is_ascii_digit())
&& !line.starts_with("//")
}
_ => false,
}
}
fn extract_name(&self, line: &str) -> Option<String> {
if let Some(paren_pos) = line.find('(') {
let before = &line[..paren_pos];
let words: Vec<&str> = before.split_whitespace().collect();
words.last().map(|s| s.trim_matches('*').to_string())
} else {
None
}
}
pub fn results(&self) -> &[AstMatchResult] {
&self.results
}
pub fn clear(&mut self) {
self.results.clear();
}
}
impl Default for ClangQuery {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefactorOperation {
Rename,
ExtractFunction,
ExtractVariable,
InlineFunction,
MoveDefinition,
ChangeSignature,
}
#[derive(Debug, Clone)]
pub struct RefactorConfig {
pub operation: RefactorOperation,
pub symbol: String,
pub new_name: Option<String>,
pub target_file: Option<String>,
pub source_range: Option<ToolingSourceRange>,
pub source_files: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RefactoringChange {
pub file: String,
pub replacements: Vec<ToolingReplacement>,
pub description: String,
}
pub struct ClangRefactor {
config: RefactorConfig,
source_manager: ToolingSourceManager,
}
impl ClangRefactor {
pub fn new(config: RefactorConfig) -> Self {
let mut sm = ToolingSourceManager::new();
for file in &config.source_files {
if let Ok(content) = fs::read_to_string(file) {
sm.add_source(file, &content);
}
}
Self {
config,
source_manager: sm,
}
}
pub fn run(&mut self) -> Vec<RefactoringChange> {
match self.config.operation {
RefactorOperation::Rename => self.rename_symbol(),
RefactorOperation::ExtractFunction => self.extract_function(),
RefactorOperation::ExtractVariable => self.extract_variable(),
_ => Vec::new(),
}
}
pub fn rename_symbol(&mut self) -> Vec<RefactoringChange> {
let old_name = &self.config.symbol.clone();
let new_name = self
.config
.new_name
.as_ref()
.cloned()
.unwrap_or_else(|| format!("{}_renamed", old_name));
let mut changes = Vec::new();
for file in &self.config.source_files.clone() {
if let Some(source) = self.source_manager.get_source(file) {
let mut replacements = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
let mut start = 0;
while let Some(pos) = line[start..].find(old_name.as_str()) {
let abs_pos = start + pos;
let is_word = {
let before = if abs_pos > 0 {
line.as_bytes()[abs_pos - 1]
} else {
b' '
};
let after = if abs_pos + old_name.len() < line.len() {
line.as_bytes()[abs_pos + old_name.len()]
} else {
b' '
};
!before.is_ascii_alphanumeric() && !after.is_ascii_alphanumeric()
};
if is_word {
let range = ToolingSourceRange::new(
SourcePosition::new(i + 1, abs_pos + 1),
SourcePosition::new(i + 1, abs_pos + old_name.len() + 1),
);
replacements.push(ToolingReplacement::new(
file,
range,
&new_name,
));
}
start = abs_pos + old_name.len().max(1);
if start >= line.len() {
break;
}
}
}
if !replacements.is_empty() {
changes.push(RefactoringChange {
file: file.clone(),
replacements,
description: format!("Rename '{}' to '{}'", old_name, new_name),
});
}
}
}
changes
}
pub fn extract_function(&mut self) -> Vec<RefactoringChange> {
let mut changes = Vec::new();
if let Some(range) = &self.config.source_range {
for file in &self.config.source_files.clone() {
if let Some(source) = self.source_manager.get_source(file) {
let new_func_name = self
.config
.new_name
.as_ref()
.cloned()
.unwrap_or_else(|| "extracted_function".to_string());
let replacement = ToolingReplacement::new(
file,
*range,
&format!("{}();", new_func_name),
);
let func_def = format!(
"void {}() {{\n // Extracted from {}\n}}\n",
new_func_name, file
);
let end_line = source.lines().count();
let insert_range = ToolingSourceRange::new(
SourcePosition::new(end_line, 1),
SourcePosition::new(end_line, 1),
);
changes.push(RefactoringChange {
file: file.clone(),
replacements: vec![
replacement,
ToolingReplacement::new(file, insert_range, &func_def),
],
description: format!("Extract code to function '{}'", new_func_name),
});
}
}
}
changes
}
pub fn extract_variable(&mut self) -> Vec<RefactoringChange> {
let mut changes = Vec::new();
if let Some(range) = &self.config.source_range {
for file in &self.config.source_files.clone() {
if let Some(source) = self.source_manager.get_source(file) {
let var_name = self
.config
.new_name
.as_ref()
.cloned()
.unwrap_or_else(|| "extracted_var".to_string());
let replacement = ToolingReplacement::new(
file,
*range,
&var_name,
);
let start_line = range.start.line;
let decl_range = ToolingSourceRange::new(
SourcePosition::new(start_line, 1),
SourcePosition::new(start_line, 1),
);
let decl = format!("auto {} = /* extracted expression */;\n", var_name);
changes.push(RefactoringChange {
file: file.clone(),
replacements: vec![
replacement,
ToolingReplacement::new(file, decl_range, &decl),
],
description: format!("Extract expression to variable '{}'", var_name),
});
}
}
}
changes
}
pub fn apply_changes(&mut self, changes: &[RefactoringChange]) -> Result<usize, String> {
for change in changes {
for r in &change.replacements {
self.source_manager.add_replacement(r.clone());
}
}
self.source_manager.save_all()
}
}
#[derive(Debug, Clone)]
pub struct ClangRenameConfig {
pub old_name: String,
pub new_name: String,
pub source_files: Vec<String>,
pub rename_in_comments: bool,
pub rename_in_strings: bool,
pub symbol_kind: Option<AstNodeKind>,
}
pub struct ClangRename {
config: ClangRenameConfig,
source_manager: ToolingSourceManager,
}
impl ClangRename {
pub fn new(config: ClangRenameConfig) -> Self {
let mut sm = ToolingSourceManager::new();
for file in &config.source_files {
if let Ok(content) = fs::read_to_string(file) {
sm.add_source(file, &content);
}
}
Self {
config,
source_manager: sm,
}
}
pub fn run(&mut self) -> Vec<RefactoringChange> {
let mut changes = Vec::new();
for file in &self.config.source_files.clone() {
if let Some(source) = self.source_manager.get_source(file) {
let mut replacements = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if self.config.rename_in_comments
&& (line.trim().starts_with("//") || line.trim().starts_with("/*"))
{
replacements.extend(self.find_and_replace_in_line(
file,
line,
i,
&self.config.old_name,
&self.config.new_name,
));
continue;
}
if self.config.rename_in_strings {
replacements.extend(self.find_and_replace_in_line(
file,
line,
i,
&self.config.old_name,
&self.config.new_name,
));
continue;
}
replacements.extend(self.find_word_replacements(
file,
line,
i,
&self.config.old_name,
&self.config.new_name,
));
}
if !replacements.is_empty() {
changes.push(RefactoringChange {
file: file.clone(),
replacements,
description: format!(
"Rename '{}' to '{}'",
self.config.old_name, self.config.new_name
),
});
}
}
}
changes
}
fn find_and_replace_in_line(
&self,
file: &str,
line: &str,
line_num: usize,
old: &str,
new: &str,
) -> Vec<ToolingReplacement> {
let mut replacements = Vec::new();
let mut start = 0;
while let Some(pos) = line[start..].find(old) {
let abs_pos = start + pos;
let range = ToolingSourceRange::new(
SourcePosition::new(line_num + 1, abs_pos + 1),
SourcePosition::new(line_num + 1, abs_pos + old.len() + 1),
);
replacements.push(ToolingReplacement::new(file, range, new));
start = abs_pos + old.len().max(1);
if start >= line.len() {
break;
}
}
replacements
}
fn find_word_replacements(
&self,
file: &str,
line: &str,
line_num: usize,
old: &str,
new: &str,
) -> Vec<ToolingReplacement> {
let mut replacements = Vec::new();
let mut start = 0;
while let Some(pos) = line[start..].find(old) {
let abs_pos = start + pos;
let before = if abs_pos > 0 {
line.as_bytes()[abs_pos - 1]
} else {
b' '
};
let after = if abs_pos + old.len() < line.len() {
line.as_bytes()[abs_pos + old.len()]
} else {
b' '
};
if !before.is_ascii_alphanumeric() && !after.is_ascii_alphanumeric() {
let range = ToolingSourceRange::new(
SourcePosition::new(line_num + 1, abs_pos + 1),
SourcePosition::new(line_num + 1, abs_pos + old.len() + 1),
);
replacements.push(ToolingReplacement::new(file, range, new));
}
start = abs_pos + old.len().max(1);
if start >= line.len() {
break;
}
}
replacements
}
pub fn apply(&mut self) -> Result<usize, String> {
let changes = self.run();
for change in &changes {
for r in &change.replacements {
self.source_manager.add_replacement(r.clone());
}
}
self.source_manager.save_all()
}
}
#[derive(Debug, Clone)]
pub struct IncludeSymbolIndex {
symbols: HashMap<String, String>,
headers: HashMap<String, Vec<String>>,
}
impl IncludeSymbolIndex {
pub fn new() -> Self {
Self {
symbols: HashMap::new(),
headers: HashMap::new(),
}
}
pub fn add_mapping(&mut self, symbol: &str, header: &str) {
self.symbols
.insert(symbol.to_string(), header.to_string());
self.headers
.entry(header.to_string())
.or_default()
.push(symbol.to_string());
}
pub fn lookup(&self, symbol: &str) -> Option<&str> {
self.symbols.get(symbol).map(|s| s.as_str())
}
pub fn load_standard_mappings(&mut self) {
let mappings = vec![
("printf", "<stdio.h>"),
("scanf", "<stdio.h>"),
("fopen", "<stdio.h>"),
("malloc", "<stdlib.h>"),
("free", "<stdlib.h>"),
("exit", "<stdlib.h>"),
("strlen", "<string.h>"),
("strcpy", "<string.h>"),
("memcpy", "<string.h>"),
("memset", "<string.h>"),
("assert", "<assert.h>"),
("sqrt", "<math.h>"),
("pow", "<math.h>"),
("sin", "<math.h>"),
("cos", "<math.h>"),
("std::vector", "<vector>"),
("std::string", "<string>"),
("std::map", "<map>"),
("std::set", "<set>"),
("std::cout", "<iostream>"),
("std::cin", "<iostream>"),
("std::cerr", "<iostream>"),
("std::unique_ptr", "<memory>"),
("std::shared_ptr", "<memory>"),
("std::make_unique", "<memory>"),
("std::make_shared", "<memory>"),
("std::thread", "<thread>"),
("std::mutex", "<mutex>"),
("std::lock_guard", "<mutex>"),
("std::sort", "<algorithm>"),
("std::find", "<algorithm>"),
("std::copy", "<algorithm>"),
("std::max", "<algorithm>"),
("std::min", "<algorithm>"),
];
for (symbol, header) in mappings {
self.add_mapping(symbol, header);
}
}
}
impl Default for IncludeSymbolIndex {
fn default() -> Self {
let mut index = Self::new();
index.load_standard_mappings();
index
}
}
pub struct ClangIncludeFixer {
index: IncludeSymbolIndex,
source_manager: ToolingSourceManager,
}
impl ClangIncludeFixer {
pub fn new() -> Self {
Self {
index: IncludeSymbolIndex::default(),
source_manager: ToolingSourceManager::new(),
}
}
pub fn add_file(&mut self, path: &str) -> Result<(), String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Cannot read {}: {}", path, e))?;
self.source_manager.add_source(path, &content);
Ok(())
}
pub fn find_missing_includes(&self, file: &str) -> Vec<String> {
let mut missing = Vec::new();
let mut existing = HashSet::new();
if let Some(source) = self.source_manager.get_source(file) {
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#include") {
if let Some(start) = trimmed.find('<') {
if let Some(end) = trimmed[start..].find('>') {
existing.insert(trimmed[start..start + end + 1].to_string());
}
}
if let Some(start) = trimmed.find('"') {
let rest = &trimmed[start + 1..];
if let Some(end) = rest.find('"') {
existing.insert(rest[..end].to_string());
}
}
}
}
for symbol in self.index.symbols.keys() {
if source.contains(symbol.as_str()) {
if let Some(header) = self.index.lookup(symbol) {
if !existing.contains(header) && !missing.contains(&header.to_string()) {
missing.push(header.to_string());
}
}
}
}
}
missing
}
pub fn generate_fixits(&self, file: &str) -> Vec<ToolingReplacement> {
let missing = self.find_missing_includes(file);
if missing.is_empty() {
return Vec::new();
}
if let Some(source) = self.source_manager.get_source(file) {
let mut last_include_line = 0;
for (i, line) in source.lines().enumerate() {
if line.trim().starts_with("#include") {
last_include_line = i + 1;
}
}
let insert_line = if last_include_line > 0 {
last_include_line + 1
} else {
1
};
let includes_str: String = missing
.iter()
.map(|h| format!("#include {}\n", h))
.collect();
let range = ToolingSourceRange::new(
SourcePosition::new(insert_line, 1),
SourcePosition::new(insert_line, 1),
);
vec![ToolingReplacement::new(file, range, &includes_str)]
} else {
Vec::new()
}
}
pub fn fix(&mut self) -> Result<usize, String> {
let files = self.source_manager.file_names();
for file in &files {
let fixits = self.generate_fixits(file);
for fixit in fixits {
self.source_manager.add_replacement(fixit);
}
}
self.source_manager.save_all()
}
}
impl Default for ClangIncludeFixer {
fn default() -> Self {
Self::new()
}
}
pub struct ClangMove {
source_manager: ToolingSourceManager,
}
impl ClangMove {
pub fn new() -> Self {
Self {
source_manager: ToolingSourceManager::new(),
}
}
pub fn add_file(&mut self, path: &str) -> Result<(), String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Cannot read {}: {}", path, e))?;
self.source_manager.add_source(path, &content);
Ok(())
}
pub fn move_function(
&mut self,
source_file: &str,
function_name: &str,
target_file: &str,
) -> Result<(), String> {
let source = self
.source_manager
.get_source(source_file)
.ok_or_else(|| format!("Source file not loaded: {}", source_file))?;
let mut func_lines = Vec::new();
let mut in_function = false;
let mut brace_count = 0;
for line in source.lines() {
if line.contains(function_name) && line.contains('(') {
in_function = true;
}
if in_function {
func_lines.push(line.to_string());
brace_count += line.matches('{').count() as i32;
brace_count -= line.matches('}').count() as i32;
if brace_count <= 0 && in_function {
break;
}
}
}
if func_lines.is_empty() {
return Err(format!("Function '{}' not found in {}", function_name, source_file));
}
let func_body = func_lines.join("\n");
let new_content = format!(
"// Moved from {}\n#include \"{}\"\n\n{}\n",
source_file,
source_file.replace(".c", ".h").replace(".cpp", ".hpp"),
func_body
);
fs::write(target_file, new_content)
.map_err(|e| format!("Cannot write {}: {}", target_file, e))?;
let start_line = source
.lines()
.position(|l| l.contains(function_name) && l.contains('('))
.map(|i| i + 1)
.unwrap_or(1);
let end_line = start_line + func_lines.len() - 1;
let range = ToolingSourceRange::new(
SourcePosition::new(start_line, 1),
SourcePosition::new(end_line + 1, 1),
);
self.source_manager
.add_replacement(ToolingReplacement::new(source_file, range, ""));
Ok(())
}
pub fn move_class(
&mut self,
source_file: &str,
class_name: &str,
target_header: &str,
) -> Result<(), String> {
let source = self
.source_manager
.get_source(source_file)
.ok_or_else(|| format!("Source file not loaded: {}", source_file))?;
let mut class_lines = Vec::new();
let mut in_class = false;
let mut brace_count = 0;
for line in source.lines() {
if line.contains("class") && line.contains(class_name) {
in_class = true;
}
if in_class {
class_lines.push(line.to_string());
brace_count += line.matches('{').count() as i32;
brace_count -= line.matches('}').count() as i32;
if brace_count <= 0 && in_class && class_lines.len() > 1 {
break;
}
}
}
if class_lines.is_empty() {
return Err(format!("Class '{}' not found in {}", class_name, source_file));
}
let header_content = format!(
"#pragma once\n\n// Moved from {}\n\n{}\n",
source_file,
class_lines.join("\n")
);
fs::write(target_header, header_content)
.map_err(|e| format!("Cannot write {}: {}", target_header, e))?;
Ok(())
}
}
impl Default for ClangMove {
fn default() -> Self {
Self::new()
}
}
pub struct ClangReorderFields;
impl ClangReorderFields {
pub fn reorder_for_size(&self, source: &str, struct_name: &str) -> Option<String> {
let lines: Vec<&str> = source.lines().collect();
let mut in_struct = false;
let mut struct_start = 0;
let mut struct_end = 0;
let mut brace_count = 0;
for (i, line) in lines.iter().enumerate() {
if line.contains("struct") && line.contains(struct_name) {
in_struct = true;
struct_start = i;
}
if in_struct {
brace_count += line.matches('{').count() as i32;
brace_count -= line.matches('}').count() as i32;
if brace_count <= 0 && in_struct {
struct_end = i;
break;
}
}
}
if !in_struct || struct_end == 0 {
return None;
}
let mut fields: Vec<(usize, String)> = Vec::new();
let mut field_indices = Vec::new();
for i in (struct_start + 1)..struct_end {
let line = lines[i].trim();
if line.is_empty()
|| line.starts_with("//")
|| line.starts_with("/*")
|| line == "{"
|| line == "}"
{
continue;
}
if line.ends_with(';') && !line.starts_with("//") {
let size_estimate = Self::estimate_type_size(line);
fields.push((size_estimate, line.to_string()));
field_indices.push(i);
}
}
if fields.len() <= 1 {
return None;
}
fields.sort_by_key(|(size, _)| std::cmp::Reverse(*size));
let sorted: Vec<String> = fields.into_iter().map(|(_, s)| s).collect();
let mut result = String::new();
for i in 0..struct_start {
result.push_str(lines[i]);
result.push('\n');
}
result.push_str(lines[struct_start]);
result.push('\n');
for field in &sorted {
result.push_str(" ");
result.push_str(field);
result.push_str("\n");
}
for i in struct_end..lines.len() {
result.push_str(lines[i]);
result.push('\n');
}
Some(result)
}
fn estimate_type_size(decl: &str) -> usize {
let decl_lower = decl.to_lowercase();
if decl_lower.contains("long long") || decl_lower.contains("double") {
8
} else if decl_lower.contains("long")
|| decl_lower.contains("int64")
|| decl_lower.contains("size_t")
{
8
} else if decl_lower.contains("int")
|| decl_lower.contains("float")
|| decl_lower.contains("unsigned")
{
4
} else if decl_lower.contains("short") || decl_lower.contains("char16") {
2
} else if decl_lower.contains("char") || decl_lower.contains("bool") {
1
} else if decl_lower.contains('*') {
8 } else {
4 }
}
pub fn reorder_alphabetically(&self, source: &str, struct_name: &str) -> Option<String> {
let lines: Vec<&str> = source.lines().collect();
let mut in_struct = false;
let mut struct_start = 0;
let mut struct_end = 0;
let mut brace_count = 0;
for (i, line) in lines.iter().enumerate() {
if line.contains("struct") && line.contains(struct_name) {
in_struct = true;
struct_start = i;
}
if in_struct {
brace_count += line.matches('{').count() as i32;
brace_count -= line.matches('}').count() as i32;
if brace_count <= 0 && in_struct {
struct_end = i;
break;
}
}
}
if !in_struct || struct_end == 0 {
return None;
}
let mut fields: Vec<String> = Vec::new();
for i in (struct_start + 1)..struct_end {
let line = lines[i].trim();
if line.is_empty()
|| line.starts_with("//")
|| line.starts_with("/*")
|| line == "{"
|| line == "}"
{
continue;
}
if line.ends_with(';') {
fields.push(line.to_string());
}
}
if fields.len() <= 1 {
return None;
}
fields.sort();
let mut result = String::new();
for i in 0..struct_start {
result.push_str(lines[i]);
result.push('\n');
}
result.push_str(lines[struct_start]);
result.push('\n');
for field in &fields {
result.push_str(" ");
result.push_str(field);
result.push_str("\n");
}
for i in struct_end..lines.len() {
result.push_str(lines[i]);
result.push('\n');
}
Some(result)
}
}
#[derive(Debug, Clone)]
pub struct RefactoringOptions {
pub create_backups: bool,
pub format_after: bool,
pub dry_run: bool,
pub max_files: Option<usize>,
pub continue_on_error: bool,
pub verbose: bool,
}
impl Default for RefactoringOptions {
fn default() -> Self {
Self {
create_backups: true,
format_after: false,
dry_run: false,
max_files: None,
continue_on_error: true,
verbose: false,
}
}
}
pub struct RefactoringTool {
options: RefactoringOptions,
source_manager: ToolingSourceManager,
results: Vec<RefactoringChange>,
}
impl RefactoringTool {
pub fn new(options: RefactoringOptions) -> Self {
Self {
options,
source_manager: ToolingSourceManager::new(),
results: Vec::new(),
}
}
pub fn add_file(&mut self, path: &str) -> Result<(), String> {
self.source_manager.load_file(Path::new(path))?;
Ok(())
}
pub fn rename(&mut self, old_name: &str, new_name: &str) -> Result<usize, String> {
let config = ClangRenameConfig {
old_name: old_name.to_string(),
new_name: new_name.to_string(),
source_files: self.source_manager.file_names(),
rename_in_comments: false,
rename_in_strings: false,
symbol_kind: None,
};
let mut renamer = ClangRename::new(config);
let changes = renamer.run();
let count = changes.len();
self.results.extend(changes);
if !self.options.dry_run {
renamer.apply()?;
}
Ok(count)
}
pub fn fix_includes(&mut self) -> Result<usize, String> {
let mut fixer = ClangIncludeFixer::new();
for file in &self.source_manager.file_names() {
fixer.add_file(file)?;
}
let count = fixer.fix()?;
Ok(count)
}
pub fn results(&self) -> &[RefactoringChange] {
&self.results
}
pub fn print_summary(&self) {
if self.results.is_empty() {
println!("No changes to apply.");
return;
}
println!("{} changes to apply:", self.results.len());
for change in &self.results {
println!(" {}: {}", change.file, change.description);
for r in &change.replacements {
println!(
" {}:{}..{} -> \"{}\"",
r.file_path,
r.range.start.line,
r.range.end.line,
r.replacement_text
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_position() {
let pos = SourcePosition::new(10, 5);
assert_eq!(pos.line, 10);
assert_eq!(pos.column, 5);
}
#[test]
fn test_source_range_contains() {
let range = ToolingSourceRange::new(
SourcePosition::new(1, 1),
SourcePosition::new(5, 1),
);
assert!(range.contains(SourcePosition::new(1, 1)));
assert!(range.contains(SourcePosition::new(3, 5)));
assert!(!range.contains(SourcePosition::new(5, 1)));
assert!(!range.contains(SourcePosition::new(5, 5)));
}
#[test]
fn test_source_range_overlap() {
let a = ToolingSourceRange::new(
SourcePosition::new(1, 1),
SourcePosition::new(5, 1),
);
let b = ToolingSourceRange::new(
SourcePosition::new(3, 1),
SourcePosition::new(7, 1),
);
assert!(a.overlaps(&b));
let c = ToolingSourceRange::new(
SourcePosition::new(6, 1),
SourcePosition::new(10, 1),
);
assert!(!a.overlaps(&c));
}
#[test]
fn test_source_manager_new() {
let sm = ToolingSourceManager::new();
assert_eq!(sm.file_count(), 0);
}
#[test]
fn test_source_manager_add_get() {
let mut sm = ToolingSourceManager::new();
sm.add_source("test.c", "int main() { return 0; }");
assert!(sm.has_file("test.c"));
assert_eq!(
sm.get_source("test.c"),
Some("int main() { return 0; }")
);
}
#[test]
fn test_replacement_single_line() {
let r = ToolingReplacement::new(
"test.c",
ToolingSourceRange::new(SourcePosition::new(1, 1), SourcePosition::new(1, 4)),
"void",
);
let result = r.apply_to("int main() { return 0; }");
assert_eq!(result, "void main() { return 0; }\n");
}
#[test]
fn test_replacement_multi_line() {
let r = ToolingReplacement::new(
"test.c",
ToolingSourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(2, 3)),
"replaced",
);
let result = r.apply_to("line1_first\nline2_second");
assert!(result.contains("replaced"));
}
#[test]
fn test_ast_node_kind_as_str() {
assert_eq!(AstNodeKind::FunctionDecl.as_str(), "functionDecl");
assert_eq!(AstNodeKind::IfStmt.as_str(), "ifStmt");
assert_eq!(AstNodeKind::StringLiteral.as_str(), "stringLiteral");
assert_eq!(AstNodeKind::CXXRecordDecl.as_str(), "cxxRecordDecl");
}
#[test]
fn test_ast_matcher_to_query_string() {
let m = AstMatcher::node_kind(AstNodeKind::FunctionDecl);
assert_eq!(m.to_query_string(), "functionDecl");
let m = AstMatcher::has_name("foo");
assert_eq!(m.to_query_string(), "hasName(\"foo\")");
let m = AstMatcher::all_of(vec![
AstMatcher::node_kind(AstNodeKind::FunctionDecl),
AstMatcher::has_name("main"),
]);
assert!(m.to_query_string().contains("allOf"));
assert!(m.to_query_string().contains("functionDecl"));
assert!(m.to_query_string().contains("hasName"));
}
#[test]
fn test_ast_matcher_helpers() {
let m = AstMatcher::has_ancestor(AstMatcher::node_kind(AstNodeKind::CXXRecordDecl));
assert!(m.to_query_string().contains("hasAncestor"));
let m = AstMatcher::has_descendant(AstMatcher::has_name("x"));
assert!(m.to_query_string().contains("hasDescendant"));
let m = AstMatcher::unless(AstMatcher::node_kind(AstNodeKind::VarDecl));
assert!(m.to_query_string().contains("unless"));
}
#[test]
fn test_ast_match_result() {
let mut result = AstMatchResult::new(
AstNodeKind::FunctionDecl,
Some("main"),
"test.cpp",
);
assert_eq!(result.name, Some("main".to_string()));
result.add_binding("returnType", "int");
assert_eq!(result.get_binding("returnType"), Some("int"));
}
#[test]
fn test_clang_check_config() {
let config = ClangCheckConfig::default();
assert!(config.enable_checks.contains(&"*".to_string()));
assert!(config.disable_checks.is_empty());
}
#[test]
fn test_clang_check_analysis() {
let config = ClangCheckConfig {
source_files: Vec::new(),
..Default::default()
};
let checker = ClangCheck::new(config);
let results = checker.run();
assert!(results.is_empty());
}
#[test]
fn test_clang_query_new() {
let mut query = ClangQuery::new();
assert!(query.results().is_empty());
query.add_matcher(AstMatcher::node_kind(AstNodeKind::FunctionDecl));
assert_eq!(query.results().len(), 0);
}
#[test]
fn test_refactoring_tool_new() {
let options = RefactoringOptions::default();
let tool = RefactoringTool::new(options);
assert!(tool.results().is_empty());
}
#[test]
fn test_refactoring_options_default() {
let opts = RefactoringOptions::default();
assert!(opts.create_backups);
assert!(!opts.format_after);
assert!(!opts.dry_run);
}
#[test]
fn test_clang_rename_config() {
let config = ClangRenameConfig {
old_name: "foo".to_string(),
new_name: "bar".to_string(),
source_files: vec!["test.c".to_string()],
rename_in_comments: false,
rename_in_strings: false,
symbol_kind: None,
};
assert_eq!(config.old_name, "foo");
assert_eq!(config.new_name, "bar");
}
#[test]
fn test_include_symbol_index_default() {
let index = IncludeSymbolIndex::default();
assert!(index.lookup("printf").is_some());
assert_eq!(index.lookup("printf"), Some("<stdio.h>"));
assert_eq!(index.lookup("std::vector"), Some("<vector>"));
assert_eq!(index.lookup("std::string"), Some("<string>"));
}
#[test]
fn test_include_symbol_index_add() {
let mut index = IncludeSymbolIndex::new();
index.add_mapping("my_func", "my_header.h");
assert_eq!(index.lookup("my_func"), Some("my_header.h"));
assert_eq!(index.lookup("unknown"), None);
}
#[test]
fn test_clang_include_fixer() {
let mut fixer = ClangIncludeFixer::new();
fixer
.source_manager
.add_source("test.c", "int main() { printf(\"hello\"); return 0; }");
let missing = fixer.find_missing_includes("test.c");
assert!(missing.contains(&"<stdio.h>".to_string()));
}
#[test]
fn test_clang_move_new() {
let mover = ClangMove::new();
assert!(mover.source_manager.file_count() == 0);
}
#[test]
fn test_clang_reorder_fields_size_estimate() {
assert_eq!(ClangReorderFields::estimate_type_size("int x;"), 4);
assert_eq!(ClangReorderFields::estimate_type_size("char c;"), 1);
assert_eq!(ClangReorderFields::estimate_type_size("double d;"), 8);
assert_eq!(ClangReorderFields::estimate_type_size("short s;"), 2);
assert_eq!(ClangReorderFields::estimate_type_size("long long ll;"), 8);
assert_eq!(ClangReorderFields::estimate_type_size("void* ptr;"), 8);
assert_eq!(ClangReorderFields::estimate_type_size("bool b;"), 1);
}
#[test]
fn test_clang_reorder_fields_reorder() {
let source = "struct Test {\n char a;\n int b;\n char c;\n double d;\n};";
let result = ClangReorderFields.reorder_for_size(source, "Test");
assert!(result.is_some());
let reordered = result.unwrap();
assert!(reordered.contains("double"));
let d_pos = reordered.find("double").unwrap();
let c_pos = reordered.find("char").unwrap();
assert!(d_pos < c_pos, "double should be before char");
}
#[test]
fn test_clang_reorder_fields_alphabetical() {
let source = "struct Test {\n int z;\n int a;\n int m;\n};";
let result = ClangReorderFields.reorder_alphabetically(source, "Test");
assert!(result.is_some());
let reordered = result.unwrap();
let a_pos = reordered.find("int a;").unwrap();
let m_pos = reordered.find("int m;").unwrap();
let z_pos = reordered.find("int z;").unwrap();
assert!(a_pos < m_pos);
assert!(m_pos < z_pos);
}
#[test]
fn test_refactoring_tool_summary() {
let options = RefactoringOptions::default();
let tool = RefactoringTool::new(options);
tool.print_summary();
}
#[test]
fn test_replacements_in_source_manager() {
let mut sm = ToolingSourceManager::new();
sm.add_source("test.c", "hello world");
sm.add_replacement(ToolingReplacement::new(
"test.c",
ToolingSourceRange::new(SourcePosition::new(1, 1), SourcePosition::new(1, 6)),
"goodbye",
));
let modified = sm.apply_replacements();
assert!(modified.contains_key("test.c"));
assert_eq!(modified["test.c"].trim(), "goodbye world");
}
#[test]
fn test_clang_check_diagnostics() {
let config = ClangCheckConfig {
source_files: Vec::new(),
..Default::default()
};
let checker = ClangCheck::new(config);
let results = checker.run();
assert!(results.is_empty());
}
#[test]
fn test_query_matcher_custom() {
let m = AstMatcher::Custom("isDefinition()".to_string());
assert_eq!(m.to_query_string(), "isDefinition()");
}
#[test]
fn test_source_range_is_empty() {
let empty = ToolingSourceRange::new(
SourcePosition::new(1, 5),
SourcePosition::new(1, 5),
);
assert!(empty.is_empty());
let non_empty = ToolingSourceRange::new(
SourcePosition::new(1, 1),
SourcePosition::new(1, 5),
);
assert!(!non_empty.is_empty());
}
#[test]
fn test_refactor_config() {
let config = RefactorConfig {
operation: RefactorOperation::Rename,
symbol: "old_name".to_string(),
new_name: Some("new_name".to_string()),
target_file: None,
source_range: None,
source_files: vec!["test.cpp".to_string()],
};
assert_eq!(config.operation, RefactorOperation::Rename);
assert_eq!(config.symbol, "old_name");
}
#[test]
fn test_refactoring_change() {
let change = RefactoringChange {
file: "test.cpp".to_string(),
replacements: vec![],
description: "test change".to_string(),
};
assert_eq!(change.file, "test.cpp");
assert_eq!(change.description, "test change");
}
}