Refactor DSL
A domain-specific language for multi-language code refactoring with Git-aware matching.
What is Refactor DSL?
Refactor DSL is a Rust library and CLI tool that provides a fluent, type-safe API for performin code refactoring operations across multiple programming languages. It combines:
- Git-aware matching - Filter repositories by branch, commit history, and working state
- Flexible file matching - Glob patterns, extensions, and content-based filtering
- AST-powered queries - Find and transform code using tree-sitter syntax patterns
- LSP integration - Semantic refactoring through Language Server Protocol
- Multi-language support - Rust, TypeScript, Python, Go, Java, C#, and Ruby
- IDE-like refactoring - Extract, Inline, Move, Change Signature, Safe Delete, Find Dead Code
- Scope analysis - Track bindings, resolve references, and analyze usage across files
- Enhanced discovery - Filter repositories by dependencies, frameworks, and metrics
Key Features
Fluent Builder API
#![allow(unused)]
fn main() {
use refactor::prelude::*;
Refactor::in_repo("./my-project")
.matching(|m| m
.git(|g| g.branch("main"))
.files(|f| f.extension("rs").exclude("**/target/**")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"TODO\")"))
.dry_run()
.apply()?;
}
Powerful Matching
- Match repositories by Git branch, commit age, remotes, and working tree state
- Match files by extension, glob patterns, content patterns, and size
- Match code structures using tree-sitter AST queries
Safe Transformations
- Preview changes with
dry_run()before applying - Generate unified diffs for review
- Atomic file operations with rollback on failure
LSP-based Semantic Refactoring
- Rename symbols with full project awareness
- Auto-install LSP servers from the Mason registry
- Support for rust-analyzer, typescript-language-server, pyright, gopls, jdtls, omnisharp, solargraph, and clangd
IDE-Like Refactoring Operations
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Extract a function from selected code
ExtractFunction::new("calculate_total")
.from_file("src/checkout.rs")
.range(Range::new(Position::new(10, 0), Position::new(20, 0)))
.visibility(Visibility::Public)
.execute()?;
// Find dead code in a workspace
let report = FindDeadCode::in_workspace("./project")
.include(DeadCodeType::UnusedFunctions)
.include(DeadCodeType::UnusedImports)
.execute()?;
// Safely delete a symbol with usage checking
SafeDelete::symbol("unused_helper")
.in_file("src/utils.rs")
.check_usages(true)
.execute()?;
}
Enhanced Repository Discovery
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Filter GitHub org repos by dependencies and frameworks
Codemod::from_github_org("acme-corp", token)
.repositories(|r| r
.has_dependency("react", ">=17.0")
.uses_framework(Framework::NextJs)
.lines_of_code(ComparisonOp::GreaterThan, 1000.0))
.apply(upgrade)
.execute()?;
}
When to Use This
Refactor DSL is ideal for:
- Codebase migrations - Update API usage patterns across many files
- Style enforcement - Apply consistent code patterns
- Bulk refactoring - Rename symbols, update imports, transform patterns
- Multi-repo operations - Apply the same changes across multiple repositories
- Automated code cleanup - Remove deprecated patterns, add missing annotations
- Code quality - Find and remove dead code, enforce naming conventions
- Safe refactoring - Extract functions, change signatures with call-site updates
Architecture
┌───────────────────────────────────────────────────────────────────────┐
│ Refactor DSL │
├───────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ ┌────────────┐ │
│ │ Matchers │ │ Transforms │ │ LSP Client │ │ Discovery │ │
│ │ │ │ │ │ │ │ │ │
│ │ - Git │ │ - Text │ │ - Rename │ │ - Deps │ │
│ │ - File │ │ - AST │ │ - References │ │ - Framework│ │
│ │ - AST │ │ - File │ │ - Definition │ │ - Metrics │ │
│ └─────────────┘ └─────────────┘ └───────────────┘ └────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Refactoring Operations │ │
│ │ │ │
│ │ Extract │ Inline │ Move │ ChangeSignature │ SafeDelete │ Dead │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Scope Analysis │ │
│ │ │ │
│ │ Bindings │ References │ Usage Analysis │ Cross-file Resolution │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Language Support │ │
│ │ │ │
│ │ Rust │ TypeScript │ Python │ Go │ Java │ C# │ Ruby │ C/C++ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ tree-sitter │ git2 │ walkdir │ globset │ regex │ lsp-types │
└───────────────────────────────────────────────────────────────────────┘
Getting Started
This guide will help you get up and running with Refactor DSL quickly.
Overview
Refactor DSL can be used in two ways:
- As a Rust library - Integrate into your Rust projects for programmatic refactoring
- As a CLI tool - Run refactoring operations from the command line
Prerequisites
- Rust 1.70+ (for building from source)
- Git (for repository operations)
Quick Example
Here’s a simple example that replaces all .unwrap() calls with .expect():
use refactor::prelude::*;
fn main() -> Result<()> {
let result = Refactor::in_repo("./my-project")
.matching(|m| m
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"TODO\")"))
.dry_run() // Preview first
.apply()?;
println!("{}", result.diff());
Ok(())
}
Or using the CLI:
refactor replace \
--pattern '\.unwrap\(\)' \
--replacement '.expect("TODO")' \
--extension rs \
--dry-run
Next Steps
- Installation - Install the library or CLI
- Quick Start - Walk through a complete example
- Matchers - Learn how to select files and code
- Transforms - Learn how to modify code
Installation
As a Rust Library
Add Refactor DSL to your Cargo.toml:
[dependencies]
refactor = "0.1"
Then import the prelude in your code:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
}
The prelude includes all commonly used types:
Refactor,MultiRepoRefactor,RefactorResultMatcher,FileMatcher,GitMatcher,AstMatcherTransform,TransformBuilder,TextTransform,AstTransformLanguage,LanguageRegistry,Rust,TypeScript,Python,Go,Java,CSharp,RubyLspClient,LspRegistry,LspRename,LspInstaller- Refactoring:
ExtractFunction,InlineVariable,MoveToFile,ChangeSignature,SafeDelete,FindDeadCode - Discovery:
DependencyFilter,FrameworkFilter,MetricFilter,LanguageFilter RefactorError,Result
As a CLI Tool
From Source
Clone and build the CLI:
git clone https://github.com/yourusername/refactor
cd refactor
cargo install --path .
Verify Installation
refactor --version
refactor languages # List supported languages
Dependencies
Refactor DSL uses these key dependencies:
| Dependency | Purpose |
|---|---|
tree-sitter | Multi-language parsing |
git2 | Git repository operations |
walkdir | File system traversal |
globset | Glob pattern matching |
regex | Regular expressions |
lsp-types | LSP protocol types |
Optional LSP Servers
For semantic refactoring (rename, find references), you’ll need language servers:
| Language | Server | Install |
|---|---|---|
| Rust | rust-analyzer | rustup component add rust-analyzer |
| TypeScript | typescript-language-server | npm i -g typescript-language-server |
| Python | pyright | npm i -g pyright |
| Go | gopls | go install golang.org/x/tools/gopls@latest |
| Java | jdtls | Eclipse JDT Language Server |
| C# | omnisharp | dotnet tool install -g OmniSharp |
| Ruby | solargraph | gem install solargraph |
| C/C++ | clangd | System package manager |
Or use auto-installation from the Mason registry (see LSP Auto-Installation).
Quick Start
This guide walks through a complete refactoring workflow.
Example: Migrating from unwrap() to expect()
Let’s say you want to replace all uses of .unwrap() with .expect("...") in a Rust project to improve error messages.
Step 1: Preview Changes
Always start with a dry run to see what will change:
use refactor::prelude::*;
fn main() -> Result<()> {
let result = Refactor::in_repo("./my-rust-project")
.matching(|m| m
.git(|g| g.branch("main")) // Only on main branch
.files(|f| f
.extension("rs")
.exclude("**/target/**")
.exclude("**/tests/**")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"TODO: handle error\")"))
.dry_run()
.apply()?;
// Print colorized diff
println!("{}", result.colorized_diff());
println!("\nSummary: {}", result.summary);
Ok(())
}
Output:
--- src/main.rs
+++ src/main.rs
@@ -10,7 +10,7 @@
fn process_file(path: &Path) -> String {
- let content = fs::read_to_string(path).unwrap();
+ let content = fs::read_to_string(path).expect("TODO: handle error");
content.trim().to_string()
}
Summary: +1 -1 in 1 file(s)
Step 2: Apply Changes
Once you’re satisfied with the preview, remove .dry_run():
#![allow(unused)]
fn main() {
let result = Refactor::in_repo("./my-rust-project")
.matching(|m| m
.files(|f| f.extension("rs").exclude("**/target/**")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"TODO: handle error\")"))
.apply()?; // Actually apply changes
println!("Modified {} files", result.files_modified());
}
Step 3: Review with Git
cd my-rust-project
git diff
git add -p # Review each change
git commit -m "Replace unwrap() with expect()"
CLI Equivalent
The same operation using the CLI:
# Preview
refactor replace \
--pattern '\.unwrap\(\)' \
--replacement '.expect("TODO: handle error")' \
--extension rs \
--exclude '**/target/**' \
--dry-run \
./my-rust-project
# Apply
refactor replace \
--pattern '\.unwrap\(\)' \
--replacement '.expect("TODO: handle error")' \
--extension rs \
--exclude '**/target/**' \
./my-rust-project
Next: More Complex Matching
See Matchers to learn how to:
- Filter by Git branch, commits, and repository state
- Use glob patterns and content matching
- Find specific code patterns with AST queries
Matchers
Matchers define predicates for selecting which repositories, files, and code patterns to include in a refactoring operation.
Overview
The Matcher type combines three kinds of matchers:
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(|m| m
.git(|g| /* Git predicates */)
.files(|f| /* File predicates */)
.ast(|a| /* AST predicates */))
.transform(/* ... */)
.apply()?;
}
Matcher Types
Git Matcher
Filter repositories by Git state:
#![allow(unused)]
fn main() {
.git(|g| g
.branch("main") // Must be on main branch
.has_file("Cargo.toml") // Must contain file
.recent_commits(30) // Commits within 30 days
.clean()) // No uncommitted changes
}
File Matcher
Filter files by path, extension, and content:
#![allow(unused)]
fn main() {
.files(|f| f
.extension("rs") // Rust files
.include("**/src/**") // Only in src/
.exclude("**/tests/**") // Exclude tests
.contains_pattern("TODO")) // Must contain TODO
}
AST Matcher
Find code patterns using tree-sitter queries:
#![allow(unused)]
fn main() {
.ast(|a| a
.query("(function_item name: (identifier) @fn)")
.capture("fn"))
}
Composition
Matchers compose with AND logic. A file must pass all configured predicates:
#![allow(unused)]
fn main() {
.matching(|m| m
.git(|g| g.branch("main")) // Repository must be on main
.files(|f| f
.extension("rs") // File must be .rs
.exclude("**/target/**"))) // AND not in target/
}
Default Behavior
If no matcher is specified, all files in the repository are included (except those in .gitignore):
#![allow(unused)]
fn main() {
// Matches all files
Refactor::in_repo("./project")
.transform(/* ... */)
.apply()?;
}
Accessing Matchers Directly
You can use matchers independently of the Refactor builder:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Check if a repo matches
let git = GitMatcher::new()
.branch("main")
.clean();
if git.matches(Path::new("./project"))? {
println!("Repo is on main and clean");
}
// Collect matching files
let files = FileMatcher::new()
.extension("rs")
.exclude("**/target/**")
.collect(Path::new("./project"))?;
println!("Found {} Rust files", files.len());
}
File Matcher
The FileMatcher filters files by extension, path patterns, content, and size.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
let matcher = FileMatcher::new()
.extension("rs")
.exclude("**/target/**");
let files = matcher.collect(Path::new("./project"))?;
}
Methods
Extension Filtering
#![allow(unused)]
fn main() {
// Single extension
.extension("rs")
// Multiple extensions
.extensions(["rs", "toml"])
// Extensions are case-insensitive
.extension("RS") // matches .rs, .Rs, .RS
}
Glob Patterns
Include and exclude files using glob patterns:
#![allow(unused)]
fn main() {
// Include only files in src/
.include("**/src/**")
// Exclude test files
.exclude("**/tests/**")
.exclude("**/*_test.rs")
// Multiple patterns
.include("**/src/**")
.include("**/lib/**")
.exclude("**/target/**")
.exclude("**/node_modules/**")
}
Glob pattern syntax:
*- Match any sequence of characters (not including/)**- Match any sequence of characters (including/)?- Match single character[abc]- Match any character in brackets[!abc]- Match any character not in brackets
Content Matching
Match files containing specific regex patterns:
#![allow(unused)]
fn main() {
// Files containing TODO comments
.contains_pattern(r"TODO:")
// Files with deprecated API usage
.contains_pattern(r"deprecated_function\(")
// Files defining public functions
.contains_pattern(r"pub fn \w+")
}
Name Matching
Match files by name using regex:
#![allow(unused)]
fn main() {
// Files starting with "test_"
.name_matches(r"^test_")
// Files ending with "_spec.rs"
.name_matches(r"_spec\.rs$")
// Main entry points
.name_matches(r"^(main|lib)\.rs$")
}
Size Filtering
Filter by file size in bytes:
#![allow(unused)]
fn main() {
// Files at least 1KB
.min_size(1024)
// Files no larger than 1MB
.max_size(1024 * 1024)
// Between 1KB and 100KB
.min_size(1024)
.max_size(100 * 1024)
}
Complete Example
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn find_large_rust_files_with_todos() -> Result<Vec<PathBuf>> {
FileMatcher::new()
.extension("rs")
.include("**/src/**")
.exclude("**/target/**")
.exclude("**/tests/**")
.contains_pattern(r"TODO|FIXME|HACK")
.min_size(1024) // At least 1KB
.collect(Path::new("./my-project"))
}
}
Integration with Refactor
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(|m| m
.files(|f| f
.extension("rs")
.exclude("**/target/**")
.contains_pattern("unwrap")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"error\")"))
.apply()?;
}
Performance Notes
Filters are applied in order of efficiency:
- Extension check (fastest)
- Glob include/exclude
- Name regex
- Size check
- Content regex (slowest - requires reading file)
Place restrictive filters early to skip expensive operations.
Git Matcher
The GitMatcher filters repositories based on Git state, including branch, commits, remotes, and working tree status.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
let matcher = GitMatcher::new()
.branch("main")
.clean();
if matcher.matches(Path::new("./project"))? {
println!("Repository is on main branch with clean working tree");
}
}
Methods
Branch Matching
#![allow(unused)]
fn main() {
// Must be on specific branch
.branch("main")
.branch("develop")
.branch("feature/my-feature")
}
File Existence
Check if specific files exist in the repository:
#![allow(unused)]
fn main() {
// Rust project
.has_file("Cargo.toml")
// Node.js project
.has_file("package.json")
// Has both (AND logic)
.has_file("Cargo.toml")
.has_file("rust-toolchain.toml")
}
Remote Matching
Check for specific Git remotes:
#![allow(unused)]
fn main() {
// Has origin remote
.has_remote("origin")
// Has upstream remote
.has_remote("upstream")
}
Commit Recency
Match repositories with recent activity:
#![allow(unused)]
fn main() {
// Has commits within last 30 days
.recent_commits(30)
// Active in last week
.recent_commits(7)
// Very recent activity
.recent_commits(1)
}
Working Tree State
#![allow(unused)]
fn main() {
// Clean working tree (no uncommitted changes)
.clean()
// Has uncommitted changes
.dirty()
// Explicit check
.has_uncommitted(true) // Same as .dirty()
.has_uncommitted(false) // Same as .clean()
}
Complete Example
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn find_active_rust_projects(workspace: &Path) -> Result<Vec<PathBuf>> {
let matcher = GitMatcher::new()
.branch("main")
.has_file("Cargo.toml")
.recent_commits(30)
.clean();
let mut matching_repos = Vec::new();
for entry in fs::read_dir(workspace)? {
let path = entry?.path();
if path.join(".git").exists() && matcher.matches(&path)? {
matching_repos.push(path);
}
}
Ok(matching_repos)
}
}
Integration with Refactor
Single Repository
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(|m| m
.git(|g| g
.branch("main")
.clean())
.files(|f| f.extension("rs")))
.transform(/* ... */)
.apply()?;
}
Multi-Repository
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")? // Find all git repos in workspace
.matching(|m| m
.git(|g| g
.has_file("Cargo.toml")
.recent_commits(30)
.clean()))
.transform(|t| t.replace_literal("old_name", "new_name"))
.apply()?;
}
Error Handling
Git matcher operations can fail if:
- Path is not a Git repository
- Repository is corrupted
- Git operations fail
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match matcher.matches(path) {
Ok(true) => println!("Matches"),
Ok(false) => println!("Does not match"),
Err(RefactorError::RepoNotFound(p)) => {
println!("Not a git repo: {}", p.display());
}
Err(e) => return Err(e.into()),
}
}
Use Cases
Only Modify Production-Ready Code
#![allow(unused)]
fn main() {
.git(|g| g
.branch("main")
.clean())
}
Find Stale Repositories
#![allow(unused)]
fn main() {
let stale = GitMatcher::new()
.has_file("Cargo.toml");
// Note: No recent_commits filter - check manually
// Then filter by age > 90 days in your code
}
Exclude Work-in-Progress
#![allow(unused)]
fn main() {
.git(|g| g
.clean() // No uncommitted changes
.has_uncommitted(false))
}
AST Matcher
The AstMatcher finds code patterns using tree-sitter queries, enabling language-aware matching that understands code structure.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
let matcher = AstMatcher::new()
.query("(function_item name: (identifier) @fn_name)");
let matches = matcher.find_matches(
"fn hello() {} fn world() {}",
&Rust,
)?;
for m in matches {
println!("Found function: {}", m.text);
}
}
Query Syntax
Tree-sitter queries use S-expression syntax. Each query pattern describes a node structure to match.
Basic Pattern
(node_type)
Matches any node of that type.
Named Children
(function_item name: (identifier))
Matches function items and accesses their name child.
Captures
(function_item name: (identifier) @fn_name)
The @fn_name captures the matched identifier for extraction.
Multiple Captures
(function_item
name: (identifier) @fn_name
parameters: (parameters) @params)
Language-Specific Queries
Rust
#![allow(unused)]
fn main() {
// Function definitions
"(function_item name: (identifier) @fn)"
// Struct definitions
"(struct_item name: (type_identifier) @struct)"
// Method calls
"(call_expression function: (field_expression field: (field_identifier) @method))"
// Use statements
"(use_declaration argument: (scoped_identifier) @import)"
}
TypeScript
#![allow(unused)]
fn main() {
// Function declarations
"(function_declaration name: (identifier) @fn)"
// Arrow functions
"(arrow_function) @arrow"
// Class declarations
"(class_declaration name: (type_identifier) @class)"
// Method definitions
"(method_definition name: (property_identifier) @method)"
}
Python
#![allow(unused)]
fn main() {
// Function definitions
"(function_definition name: (identifier) @fn)"
// Class definitions
"(class_definition name: (identifier) @class)"
// Import statements
"(import_statement name: (dotted_name) @import)"
// Function calls
"(call function: (identifier) @call)"
}
Methods
Adding Queries
#![allow(unused)]
fn main() {
// Single query
.query("(function_item name: (identifier) @fn)")
// Multiple queries
.query("(function_item name: (identifier) @fn)")
.query("(struct_item name: (type_identifier) @struct)")
}
Filtering Captures
#![allow(unused)]
fn main() {
// Only get specific captures
.query("(function_item name: (identifier) @fn)")
.query("(parameter pattern: (identifier) @param)")
.capture("fn") // Only return @fn captures
}
Match Results
Each match contains:
#![allow(unused)]
fn main() {
pub struct AstMatch {
pub text: String, // Matched text
pub start_byte: usize, // Byte offset start
pub end_byte: usize, // Byte offset end
pub start_row: usize, // Line number (0-indexed)
pub start_col: usize, // Column (0-indexed)
pub end_row: usize,
pub end_col: usize,
pub capture_name: String, // The @name used
}
}
File-Based Matching
#![allow(unused)]
fn main() {
let matcher = AstMatcher::new()
.query("(function_item name: (identifier) @fn)");
let registry = LanguageRegistry::new();
let matches = matcher.find_matches_in_file(
Path::new("src/main.rs"),
®istry,
)?;
}
Check for Matches
#![allow(unused)]
fn main() {
// Just check if pattern exists
if matcher.has_matches(source, &Rust)? {
println!("Found matching code");
}
}
Complete Example
Find all functions that call unwrap():
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn find_unwrap_calls(path: &Path) -> Result<Vec<AstMatch>> {
let source = fs::read_to_string(path)?;
let matcher = AstMatcher::new()
.query(r#"
(call_expression
function: (field_expression
field: (field_identifier) @method)
(#eq? @method "unwrap"))
"#);
matcher.find_matches(&source, &Rust)
}
}
Integration with Refactor
The AST matcher can be used with the Refactor builder, but note that AST matching is primarily for finding patterns, not filtering files in the current implementation:
#![allow(unused)]
fn main() {
// Use FileMatcher with content patterns for filtering
Refactor::in_repo("./project")
.matching(|m| m
.files(|f| f
.extension("rs")
.contains_pattern(r"\.unwrap\(\)")))
.transform(/* ... */)
.apply()?;
// Use AstMatcher separately for precise code analysis
let ast_matcher = AstMatcher::new()
.query("(call_expression) @call");
}
Finding Query Patterns
To discover the correct query patterns for your language:
- Use the tree-sitter CLI:
tree-sitter parse file.rs - Use the tree-sitter playground
- Check grammar definitions in tree-sitter-{language} repositories
Error Handling
Invalid queries return an error:
#![allow(unused)]
fn main() {
let matcher = AstMatcher::new()
.query("(invalid_node_type @x)");
match matcher.find_matches(source, &Rust) {
Ok(matches) => { /* ... */ }
Err(RefactorError::Query(_)) => {
println!("Invalid query syntax");
}
Err(e) => return Err(e),
}
}
Transforms
Transforms define how to modify matched code. Refactor DSL supports text-based transformations with regex patterns and AST-aware transformations.
Overview
Transforms are composed using the TransformBuilder:
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(/* ... */)
.transform(|t| t
.replace_pattern(r"old_api\(\)", "new_api()")
.replace_literal("OldName", "NewName"))
.apply()?;
}
Transform Types
Text Transforms
Pattern-based text replacement:
#![allow(unused)]
fn main() {
.transform(|t| t
// Regex replacement
.replace_pattern(r"\.unwrap\(\)", ".expect(\"error\")")
// Literal string replacement
.replace_literal("old_name", "new_name"))
}
AST Transforms
Structure-aware code transformations:
#![allow(unused)]
fn main() {
.transform(|t| t
.ast(|a| a
.query("(function_item name: (identifier) @fn)")
.transform(|node| /* modify node */)))
}
Transform Trait
All transforms implement the Transform trait:
#![allow(unused)]
fn main() {
pub trait Transform: Send + Sync {
/// Applies the transformation to source code.
fn apply(&self, source: &str, path: &Path) -> Result<String>;
/// Returns a human-readable description.
fn describe(&self) -> String;
}
}
Custom Transforms
Implement the Transform trait for custom behavior:
#![allow(unused)]
fn main() {
use refactor::transform::Transform;
use refactor::error::Result;
use std::path::Path;
struct UppercaseTransform;
impl Transform for UppercaseTransform {
fn apply(&self, source: &str, _path: &Path) -> Result<String> {
Ok(source.to_uppercase())
}
fn describe(&self) -> String {
"Convert to uppercase".to_string()
}
}
// Use with the builder
Refactor::in_repo("./project")
.transform(|t| t.custom(UppercaseTransform))
.apply()?;
}
Transform Composition
Multiple transforms are applied in order:
#![allow(unused)]
fn main() {
.transform(|t| t
.replace_pattern(r"foo", "bar") // Applied first
.replace_pattern(r"bar", "baz") // Applied second
.replace_literal("baz", "qux")) // Applied third
// "foo" -> "bar" -> "baz" -> "qux"
}
Preview and Description
Get descriptions of configured transforms:
#![allow(unused)]
fn main() {
let builder = TransformBuilder::new()
.replace_pattern(r"old", "new")
.replace_literal("foo", "bar");
for desc in builder.describe() {
println!("{}", desc);
}
// Output:
// Replace pattern 'old' with 'new'
// Replace literal 'foo' with 'bar'
}
Dry Run
Always preview changes before applying:
#![allow(unused)]
fn main() {
let result = Refactor::in_repo("./project")
.matching(/* ... */)
.transform(/* ... */)
.dry_run() // Preview only
.apply()?;
println!("{}", result.diff());
}
Result Inspection
After applying transforms:
#![allow(unused)]
fn main() {
let result = refactor.apply()?;
// Number of files changed
println!("Modified {} files", result.files_modified());
// Detailed diff
println!("{}", result.diff());
// Colorized diff for terminal
println!("{}", result.colorized_diff());
// Change summary
println!("{}", result.summary);
}
FileChange Details
Each modified file produces a FileChange:
#![allow(unused)]
fn main() {
for change in &result.changes {
if change.is_modified() {
println!("Modified: {}", change.path.display());
// change.original - Original content
// change.transformed - New content
}
}
}
Text Transforms
Text transforms modify source code using pattern matching and replacement. They work on raw text without understanding code structure.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
let transform = TextTransform::replace(r"\.unwrap\(\)", ".expect(\"error\")");
let result = transform.apply(source, Path::new("file.rs"))?;
}
Transform Types
Regex Replacement
Replace text matching a regex pattern:
#![allow(unused)]
fn main() {
// Basic replacement
TextTransform::replace(r"old_api", "new_api")
// With capture groups
TextTransform::replace(r"fn (\w+)", "pub fn $1")
// From pre-compiled regex
let pattern = Regex::new(r"\d+")?;
TextTransform::replace_regex(pattern, "NUM")
}
Regex syntax follows the regex crate.
Capture group reference:
$1,$2, etc. - Numbered groups$name- Named groups (if using(?P<name>...))
Literal Replacement
Replace exact text without regex interpretation:
#![allow(unused)]
fn main() {
// Safe for special characters
TextTransform::replace_literal("Vec<T>", "Vec<U>")
// Won't interpret .* as regex
TextTransform::replace_literal(".*", "WILDCARD")
}
Line Operations
Prepend to Lines
Add text before matching lines:
#![allow(unused)]
fn main() {
// Add comment before function definitions
TextTransform::prepend_line(r"^\s*fn ", "// TODO: document\n")?
// Add attribute before test functions
TextTransform::prepend_line(r"^\s*fn test_", "#[ignore]\n")?
}
Append to Lines
Add text after matching lines:
#![allow(unused)]
fn main() {
// Add comment after statements
TextTransform::append_line(r";\s*$", " // reviewed")?
// Add semicolons to lines ending in certain patterns
TextTransform::append_line(r"\)$", ";")?
}
Delete Lines
Remove lines matching a pattern:
#![allow(unused)]
fn main() {
// Remove comment lines
TextTransform::delete_lines(r"^\s*//")?
// Remove empty lines
TextTransform::delete_lines(r"^\s*$")?
// Remove debug statements
TextTransform::delete_lines(r"console\.log\(")?
}
Insert After Lines
Insert content after matching lines:
#![allow(unused)]
fn main() {
// Add blank line after imports
TextTransform::insert_after(r"^use ", "")?
// Add attribute after doc comments
TextTransform::insert_after(r"^///", "#[doc(hidden)]")?
}
Insert Before Lines
Insert content before matching lines:
#![allow(unused)]
fn main() {
// Add attribute before functions
TextTransform::insert_before(r"^fn ", "#[inline]")?
// Add header comment before module declaration
TextTransform::insert_before(r"^mod ", "// Module:\n")?
}
Using with TransformBuilder
The TransformBuilder provides convenient methods:
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.transform(|t| t
// Regex replacement
.replace_pattern(r"old_api\(\)", "new_api()")
// Literal replacement
.replace_literal("OldType", "NewType"))
.apply()?;
}
Complete Examples
Replace Deprecated API
#![allow(unused)]
fn main() {
let result = Refactor::in_repo("./project")
.matching(|m| m.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(
r"deprecated_function\((.*?)\)",
"new_function($1, Default::default())"
))
.apply()?;
}
Add Missing Attributes
#![allow(unused)]
fn main() {
use refactor::transform::TextTransform;
// Add #[derive(Debug)] before struct definitions that don't have it
let transform = TextTransform::insert_before(
r"^pub struct \w+",
"#[derive(Debug)]\n"
)?;
}
Clean Up Comments
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(|m| m.files(|f| f.extension("rs")))
.transform(|t| t
.custom(TextTransform::delete_lines(r"^\s*// TODO:").unwrap()))
.apply()?;
}
Rename with Context
#![allow(unused)]
fn main() {
// Rename function but preserve formatting
Refactor::in_repo("./project")
.transform(|t| t
.replace_pattern(
r"fn\s+old_function\s*\(",
"fn new_function("
)
.replace_pattern(
r"old_function\s*\(",
"new_function("
))
.apply()?;
}
Error Handling
Invalid regex patterns return errors:
#![allow(unused)]
fn main() {
use refactor::transform::TextTransform;
// This will panic - invalid regex
// TextTransform::replace(r"[invalid", "replacement")
// Use try methods for fallible patterns
let result = TextTransform::delete_lines(r"[");
assert!(result.is_err());
}
Performance Tips
- Use literal replacement when possible - Faster than regex
- Be specific with patterns -
^\s*fnis faster thanfn - Order transforms efficiently - Put common replacements first
- Combine patterns when possible - One regex with
|vs multiple passes
#![allow(unused)]
fn main() {
// Slower: Multiple passes
.replace_pattern(r"foo", "qux")
.replace_pattern(r"bar", "qux")
.replace_pattern(r"baz", "qux")
// Faster: Single pass
.replace_pattern(r"foo|bar|baz", "qux")
}
AST Transforms
AST transforms modify code with awareness of its syntactic structure, using tree-sitter for parsing.
Note: AST transforms in the current version are primarily used for matching and analysis. For structural code modifications, consider using LSP-based refactoring which provides semantic understanding.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
Refactor::in_repo("./project")
.transform(|t| t
.ast(|a| a
.query("(function_item name: (identifier) @fn)")
.transform(|node| /* modify */)))
.apply()?;
}
AstTransform Builder
The AstTransform type allows configuring AST-based transformations:
#![allow(unused)]
fn main() {
let transform = AstTransform::new()
.query("(function_item name: (identifier) @fn)")
.language(&Rust);
}
Query-Based Matching
AST transforms start with a tree-sitter query to identify code patterns:
#![allow(unused)]
fn main() {
// Find all function definitions
.query("(function_item name: (identifier) @fn)")
// Find struct definitions
.query("(struct_item name: (type_identifier) @struct)")
// Find method calls
.query("(call_expression
function: (field_expression field: (field_identifier) @method))")
}
Common Patterns
Finding Functions
#![allow(unused)]
fn main() {
// Rust
"(function_item name: (identifier) @fn)"
// TypeScript
"(function_declaration name: (identifier) @fn)"
// Python
"(function_definition name: (identifier) @fn)"
}
Finding Classes/Structs
#![allow(unused)]
fn main() {
// Rust
"(struct_item name: (type_identifier) @name)"
"(impl_item type: (type_identifier) @impl_type)"
// TypeScript
"(class_declaration name: (type_identifier) @class)"
// Python
"(class_definition name: (identifier) @class)"
}
Finding Imports
#![allow(unused)]
fn main() {
// Rust
"(use_declaration argument: (_) @import)"
// TypeScript
"(import_statement source: (string) @source)"
// Python
"(import_statement name: (dotted_name) @import)"
}
Integration with Text Transforms
AST matching can identify locations for text-based replacement:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn rename_function(source: &str, old_name: &str, new_name: &str) -> Result<String> {
// First, find all occurrences using AST
let matcher = AstMatcher::new()
.query("(function_item name: (identifier) @fn)")
.query("(call_expression function: (identifier) @call)");
let matches = matcher.find_matches(source, &Rust)?;
// Filter to our target function
let target_matches: Vec<_> = matches
.iter()
.filter(|m| m.text == old_name)
.collect();
// Apply text replacement at those positions
let mut result = source.to_string();
for m in target_matches.iter().rev() {
result.replace_range(m.start_byte..m.end_byte, new_name);
}
Ok(result)
}
}
Combining with File Matching
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching(|m| m
.files(|f| f
.extension("rs")
.exclude("**/target/**"))
.ast(|a| a
.query("(function_item) @fn")))
.transform(|t| t
.replace_pattern(r"fn (\w+)", "pub fn $1"))
.apply()?;
}
Language Detection
AST transforms require knowing the source language:
#![allow(unused)]
fn main() {
// Explicit language
let transform = AstTransform::new()
.query("(function_item @fn)")
.language(&Rust);
// Or detect from file extension
let registry = LanguageRegistry::new();
let lang = registry.detect(Path::new("file.rs")).unwrap();
}
Limitations
Current AST transform limitations:
- Read-only analysis - AST queries find patterns but don’t directly modify the tree
- No semantic understanding - Doesn’t understand types, scopes, or references
- Single-file scope - Can’t follow imports or understand project structure
For semantic refactoring (cross-file renames, reference updates), use LSP integration.
Future Directions
Planned enhancements:
- Direct AST node manipulation
- Structural search and replace
- Code generation from AST templates
- Multi-file AST analysis
See Also
- AST Matcher - Finding code patterns
- Tree-sitter Queries - Query syntax reference
- LSP Integration - Semantic refactoring
Language Support
Refactor DSL provides multi-language support through tree-sitter parsers, enabling AST-based matching and transforms across different programming languages.
Built-in Languages
| Language | Extensions | Tree-sitter Grammar | LSP Server |
|---|---|---|---|
| Rust | .rs | tree-sitter-rust | rust-analyzer |
| TypeScript | .ts, .tsx, .js, .jsx | tree-sitter-typescript | typescript-language-server |
| Python | .py, .pyi | tree-sitter-python | pyright |
| Go | .go | tree-sitter-go | gopls |
| Java | .java | tree-sitter-java | jdtls |
| C# | .cs | tree-sitter-c-sharp | omnisharp |
| Ruby | .rb, .rake, .gemspec | tree-sitter-ruby | solargraph |
| C/C++ | .c, .h, .cpp, .hpp | tree-sitter-cpp | clangd |
Language Registry
The LanguageRegistry manages available languages:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
let registry = LanguageRegistry::new();
// Find by extension
let rust = registry.by_extension("rs");
// Find by name
let python = registry.by_name("python");
// Detect from file path
let lang = registry.detect(Path::new("src/main.rs"));
// List all languages
for lang in registry.all() {
println ! ("{}: {:?}", lang.name(), lang.extensions());
}
}
Using Languages Directly
Each language implements the Language trait:
use refactor::lang::{Rust, TypeScript, Python};
// Parse source code
let tree = Rust.parse("fn main() {}") ?;
// Create a query
let query = Rust.query("(function_item name: (identifier) @fn)") ?;
// Check extension
assert!(Rust.matches_extension("rs"));
assert!(TypeScript.matches_extension("tsx"));
Language Trait
#![allow(unused)]
fn main() {
pub trait Language: Send + Sync {
/// Language name (lowercase)
fn name(&self) -> &'static str;
/// File extensions handled
fn extensions(&self) -> &[&'static str];
/// Tree-sitter grammar
fn grammar(&self) -> tree_sitter::Language;
/// Parse source into AST
fn parse(&self, source: &str) -> Result<Tree>;
/// Create a tree-sitter query
fn query(&self, pattern: &str) -> Result<Query>;
/// Check if extension matches
fn matches_extension(&self, ext: &str) -> bool;
}
}
Language-Specific Examples
Rust
#![allow(unused)]
fn main() {
use refactor::lang::Rust;
let source = r#"
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
"#;
// Parse
let tree = Rust.parse(source) ?;
assert!(!tree.root_node().has_error());
// Query for functions
let matcher = AstMatcher::new()
.query("(function_item name: (identifier) @fn)");
let matches = matcher.find_matches(source, & Rust) ?;
}
Common Rust queries:
#![allow(unused)]
fn main() {
// Functions
"(function_item name: (identifier) @fn)"
// Structs
"(struct_item name: (type_identifier) @struct)"
// Enums
"(enum_item name: (type_identifier) @enum)"
// Impl blocks
"(impl_item type: (type_identifier) @impl)"
// Use statements
"(use_declaration) @use"
// Macro invocations
"(macro_invocation macro: (identifier) @macro)"
}
TypeScript
#![allow(unused)]
fn main() {
use refactor::lang::TypeScript;
let source = r#"
function greet(name: string): string {
return `Hello, ${name}!`;
}
const arrow = (x: number) => x * 2;
"#;
let tree = TypeScript.parse(source) ?;
let matcher = AstMatcher::new()
.query("(function_declaration name: (identifier) @fn)");
let matches = matcher.find_matches(source, & TypeScript) ?;
}
Common TypeScript queries:
#![allow(unused)]
fn main() {
// Functions
"(function_declaration name: (identifier) @fn)"
// Arrow functions
"(arrow_function) @arrow"
// Classes
"(class_declaration name: (type_identifier) @class)"
// Interfaces
"(interface_declaration name: (type_identifier) @interface)"
// Type aliases
"(type_alias_declaration name: (type_identifier) @type)"
// Imports
"(import_statement) @import"
}
Python
#![allow(unused)]
fn main() {
use refactor::lang::Python;
let source = r#"
def greet(name: str) -> str:
return f"Hello, {name}!"
class Greeter:
def say_hello(self):
print("Hello!")
"#;
let tree = Python.parse(source) ?;
let matcher = AstMatcher::new()
.query("(function_definition name: (identifier) @fn)");
let matches = matcher.find_matches(source, & Python) ?;
}
Common Python queries:
#![allow(unused)]
fn main() {
// Functions
"(function_definition name: (identifier) @fn)"
// Classes
"(class_definition name: (identifier) @class)"
// Methods (inside class)
"(class_definition body: (block (function_definition name: (identifier) @method)))"
// Imports
"(import_statement) @import"
"(import_from_statement) @from_import"
// Decorators
"(decorated_definition decorator: (decorator) @decorator)"
}
Go
#![allow(unused)]
fn main() {
use refactor::lang::Go;
let source = r#"
package main
import "fmt"
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
type Greeter struct {
Name string
}
func (g *Greeter) SayHello() {
fmt.Printf("Hello from %s\n", g.Name)
}
"#;
let tree = Go.parse(source) ?;
let matcher = AstMatcher::new()
.query("(function_declaration name: (identifier) @fn)");
let matches = matcher.find_matches(source, & Go) ?;
}
Common Go queries:
#![allow(unused)]
fn main() {
// Functions
"(function_declaration name: (identifier) @fn)"
// Methods (with receiver)
"(method_declaration
receiver: (parameter_list) @receiver
name: (field_identifier) @method)"
// Structs
"(type_declaration (type_spec name: (type_identifier) @struct type: (struct_type)))"
// Interfaces
"(type_declaration (type_spec name: (type_identifier) @interface type: (interface_type)))"
// Imports
"(import_declaration) @import"
"(import_spec path: (interpreted_string_literal) @path)"
// Package declaration
"(package_clause (package_identifier) @package)"
}
Java
#![allow(unused)]
fn main() {
use refactor::lang::Java;
let source = r#"
package com.example;
import java.util.List;
public class Greeter {
private String name;
public Greeter(String name) {
this.name = name;
}
public String greet() {
return "Hello, " + name + "!";
}
}
"#;
let tree = Java.parse(source) ?;
let matcher = AstMatcher::new()
.query("(class_declaration name: (identifier) @class)");
let matches = matcher.find_matches(source, & Java) ?;
}
Common Java queries:
#![allow(unused)]
fn main() {
// Classes
"(class_declaration name: (identifier) @class)"
// Interfaces
"(interface_declaration name: (identifier) @interface)"
// Methods
"(method_declaration name: (identifier) @method)"
// Constructors
"(constructor_declaration name: (identifier) @constructor)"
// Fields
"(field_declaration declarator: (variable_declarator name: (identifier) @field))"
// Imports
"(import_declaration) @import"
// Annotations
"(annotation name: (identifier) @annotation)"
// Package declaration
"(package_declaration) @package"
}
C#
#![allow(unused)]
fn main() {
use refactor::lang::CSharp;
let source = r#"
using System;
namespace Example
{
public class Greeter
{
public string Name { get; set; }
public string Greet()
{
return $"Hello, {Name}!";
}
}
}
"#;
let tree = CSharp.parse(source) ?;
let matcher = AstMatcher::new()
.query("(class_declaration name: (identifier) @class)");
let matches = matcher.find_matches(source, & CSharp) ?;
}
Common C# queries:
#![allow(unused)]
fn main() {
// Classes
"(class_declaration name: (identifier) @class)"
// Interfaces
"(interface_declaration name: (identifier) @interface)"
// Methods
"(method_declaration name: (identifier) @method)"
// Properties
"(property_declaration name: (identifier) @property)"
// Fields
"(field_declaration (variable_declaration (variable_declarator (identifier) @field)))"
// Using directives
"(using_directive) @using"
// Namespaces
"(namespace_declaration name: (_) @namespace)"
// Records (C# 9+)
"(record_declaration name: (identifier) @record)"
}
Ruby
#![allow(unused)]
fn main() {
use refactor::lang::Ruby;
let source = r#"
class Greeter
attr_accessor :name
def initialize(name)
@name = name
end
def greet
"Hello, #{@name}!"
end
end
module Helpers
def self.format_name(name)
name.capitalize
end
end
"#;
let tree = Ruby.parse(source) ?;
let matcher = AstMatcher::new()
.query("(class name: (constant) @class)");
let matches = matcher.find_matches(source, & Ruby) ?;
}
Common Ruby queries:
#![allow(unused)]
fn main() {
// Classes
"(class name: (constant) @class)"
// Modules
"(module name: (constant) @module)"
// Methods
"(method name: (identifier) @method)"
// Singleton methods (class methods)
"(singleton_method name: (identifier) @class_method)"
// Instance variables
"(instance_variable) @ivar"
// Requires
"(call method: (identifier) @method (#eq? @method \"require\"))"
// attr_accessor, attr_reader, attr_writer
"(call method: (identifier) @accessor (#match? @accessor \"^attr_\"))"
// Blocks
"(block) @block"
"(do_block) @do_block"
}
Adding Custom Languages
To add support for a new language:
- Add the tree-sitter grammar dependency to
Cargo.toml - Implement the
Languagetrait - Register with
LanguageRegistry
#![allow(unused)]
fn main() {
use refactor::lang::Language;
use tree_sitter::Language as TsLanguage;
// Add to Cargo.toml:
// tree-sitter-go = "0.20"
pub struct Go;
impl Language for Go {
fn name(&self) -> &'static str {
"go"
}
fn extensions(&self) -> &[&'static str] {
&["go"]
}
fn grammar(&self) -> TsLanguage {
tree_sitter_go::LANGUAGE.into()
}
}
// Register
let mut registry = LanguageRegistry::new();
registry.register(Box::new(Go));
}
Language Detection
Automatic language detection from file paths:
#![allow(unused)]
fn main() {
let registry = LanguageRegistry::new();
// Detects by extension
let lang = registry.detect(Path::new("src/main.rs"));
assert_eq!(lang.unwrap().name(), "rust");
let lang = registry.detect(Path::new("app/index.tsx"));
assert_eq!(lang.unwrap().name(), "typescript");
// No extension or unknown
let lang = registry.detect(Path::new("Makefile"));
assert!(lang.is_none());
}
Integration with Refactor
Language detection happens automatically in the refactor pipeline:
#![allow(unused)]
fn main() {
Refactor::in_repo("./project")
.matching( | m| m
.files( | f| f.extension("rs")) // Language inferred from extension
.ast( | a| a.query("(function_item @fn)"))) // Rust query
.transform(/* ... */)
.apply() ?;
}
For explicit language specification:
#![allow(unused)]
fn main() {
let matcher = AstMatcher::new()
.query("(function_item @fn)");
// Explicit language
let matches = matcher.find_matches(source, & Rust) ?;
// Or use registry
let registry = LanguageRegistry::new();
let lang = registry.by_name("rust").unwrap();
let matches = matcher.find_matches(source, lang) ?;
}
Refactoring Operations
Refactor DSL provides IDE-like refactoring operations that understand code structure, track references, and update all affected locations automatically.
Overview
Unlike simple text transforms, refactoring operations:
- Understand scope - Track variable bindings and visibility
- Resolve references - Find all usages across files
- Maintain correctness - Update imports, call sites, and signatures
- Preview changes - Dry-run mode shows what will change
- Validate safety - Check for conflicts before applying
Available Operations
Extract
Extract code into new functions, variables, or constants:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Extract lines 10-20 into a new function
ExtractFunction::new("calculate_total")
.from_file("src/checkout.rs")
.range(Range::new(Position::new(10, 0), Position::new(20, 0)))
.visibility(Visibility::Public)
.execute()?;
// Extract repeated expression into a variable
ExtractVariable::new("tax_rate")
.from_file("src/pricing.rs")
.position(Position::new(15, 20))
.replace_all_occurrences(true)
.execute()?;
// Extract magic number into a named constant
ExtractConstant::new("MAX_RETRIES")
.from_file("src/client.rs")
.position(Position::new(8, 25))
.execute()?;
}
Inline
Inline variables and functions back into their usage sites:
#![allow(unused)]
fn main() {
// Inline a variable (replace usages with its value)
InlineVariable::new("temp")
.in_file("src/process.rs")
.position(Position::new(12, 8))
.execute()?;
// Inline a function (replace calls with function body)
InlineFunction::new("helper")
.in_file("src/utils.rs")
.all_call_sites(true)
.execute()?;
}
Move
Move code between files and modules:
#![allow(unused)]
fn main() {
// Move a function to another file
MoveToFile::new("process_data")
.from_file("src/utils.rs")
.to_file("src/processors.rs")
.update_imports(true)
.execute()?;
// Move between modules with re-exports
MoveBetweenModules::new("DataProcessor")
.from_module("crate::utils")
.to_module("crate::processors")
.add_reexport(true)
.execute()?;
}
Change Signature
Modify function signatures with call-site updates:
#![allow(unused)]
fn main() {
// Add a new parameter with default value
ChangeSignature::for_function("process")
.in_file("src/lib.rs")
.add_parameter("timeout", "Duration", "Duration::from_secs(30)")
.execute()?;
// Reorder parameters
ChangeSignature::for_function("create_user")
.in_file("src/users.rs")
.reorder_parameters(&["name", "email", "role"])
.execute()?;
// Remove unused parameter
ChangeSignature::for_function("old_api")
.in_file("src/legacy.rs")
.remove_parameter("unused_flag")
.execute()?;
}
Safe Delete
Remove code with usage checking:
#![allow(unused)]
fn main() {
// Delete a function, warning if it has usages
SafeDelete::symbol("unused_helper")
.in_file("src/utils.rs")
.check_usages(true)
.execute()?;
// Delete with cascade (also delete dependent code)
SafeDelete::symbol("deprecated_module")
.in_file("src/lib.rs")
.cascade(true)
.execute()?;
}
Find Dead Code
Analyze and report unused code:
#![allow(unused)]
fn main() {
// Find all dead code in workspace
let report = FindDeadCode::in_workspace("./project")
.include(DeadCodeType::UnusedFunctions)
.include(DeadCodeType::UnusedImports)
.include(DeadCodeType::UnreachableCode)
.execute()?;
// Print the report
for item in &report.items {
println!("{}: {} at {}:{}",
item.kind,
item.name,
item.file.display(),
item.line);
}
// Generate JSON report
let json = report.to_json()?;
}
Refactoring Context
All operations share a common RefactoringContext that provides:
#![allow(unused)]
fn main() {
pub struct RefactoringContext {
pub workspace_root: PathBuf,
pub target_file: PathBuf,
pub target_range: Range,
pub language: Box<dyn Language>,
pub scope_analyzer: ScopeAnalyzer,
pub lsp_client: Option<LspClient>,
}
}
Common Patterns
Preview Before Apply
Always preview changes first:
#![allow(unused)]
fn main() {
let result = ExtractFunction::new("helper")
.from_file("src/main.rs")
.range(selection)
.dry_run() // Preview only
.execute()?;
println!("Preview:\n{}", result.diff());
// If satisfied, apply
if user_confirmed() {
ExtractFunction::new("helper")
.from_file("src/main.rs")
.range(selection)
.execute()?; // Actually apply
}
}
Batch Operations
Apply multiple refactorings in sequence:
#![allow(unused)]
fn main() {
// Find and fix all issues
let dead_code = FindDeadCode::in_workspace("./project")
.include(DeadCodeType::UnusedFunctions)
.execute()?;
for item in dead_code.items {
SafeDelete::symbol(&item.name)
.in_file(&item.file)
.execute()?;
}
}
With LSP Enhancement
Use LSP for better accuracy:
#![allow(unused)]
fn main() {
ExtractFunction::new("process")
.from_file("src/main.rs")
.range(selection)
.use_lsp(true) // Use LSP for type inference
.auto_install_lsp(true) // Install LSP if needed
.execute()?;
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match SafeDelete::symbol("helper").in_file("src/lib.rs").execute() {
Ok(result) => println!("Deleted successfully"),
Err(RefactorError::SymbolInUse { usages }) => {
println!("Cannot delete: {} usages found", usages.len());
for usage in usages {
println!(" - {}:{}", usage.file.display(), usage.line);
}
}
Err(RefactorError::SymbolNotFound(name)) => {
println!("Symbol '{}' not found", name);
}
Err(e) => return Err(e.into()),
}
}
Validation
Operations validate before executing:
#![allow(unused)]
fn main() {
let result = ExtractFunction::new("helper")
.from_file("src/main.rs")
.range(selection)
.validate()?; // Just validate, don't execute
match result {
ValidationResult::Valid => println!("Ready to extract"),
ValidationResult::Warning(msg) => println!("Warning: {}", msg),
ValidationResult::Invalid(msg) => println!("Cannot extract: {}", msg),
}
}
See Also
- Scope Analysis - How bindings and references are tracked
- LSP Integration - Enhanced refactoring with LSP
- Transforms - Simpler text-based transforms
Extract Operations
Extract operations create new named entities (functions, variables, constants) from existing code, making it more modular and reusable.
ExtractFunction
Extract a block of code into a new function.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Extract lines 10-20 into a new function
let result = ExtractFunction::new("calculate_total")
.from_file("src/checkout.rs")
.range(Range::new(
Position::new(10, 0),
Position::new(20, 0)
))
.execute()?;
println!("Created function: {}", result.function_name);
}
Configuration
#![allow(unused)]
fn main() {
ExtractFunction::new("process_items")
.from_file("src/processor.rs")
.range(selection)
// Set visibility
.visibility(Visibility::Public) // pub fn
// .visibility(Visibility::Crate) // pub(crate) fn
// .visibility(Visibility::Private) // fn (default)
// Control parameter inference
.parameter_strategy(ParameterStrategy::Infer) // Auto-detect (default)
// .parameter_strategy(ParameterStrategy::Explicit(params)) // Specify manually
// Add documentation
.with_doc("Processes items and returns the result")
// Dry run first
.dry_run()
.execute()?;
}
Parameter Inference
The extractor analyzes the selected code to determine:
- Parameters - Variables used but not defined in the selection
- Return value - Values computed and used after the selection
- Mutability - Whether parameters need
&mut
// Before extraction:
fn main() {
let items = vec![1, 2, 3];
let multiplier = 2;
// --- Selection start ---
let mut sum = 0;
for item in &items {
sum += item * multiplier;
}
// --- Selection end ---
println!("Sum: {}", sum);
}
// After extraction:
fn calculate_sum(items: &[i32], multiplier: i32) -> i32 {
let mut sum = 0;
for item in items {
sum += item * multiplier;
}
sum
}
fn main() {
let items = vec![1, 2, 3];
let multiplier = 2;
let sum = calculate_sum(&items, multiplier);
println!("Sum: {}", sum);
}
Validation
The extractor validates that:
- Selection contains complete statements
- No partial expressions are selected
- Control flow (return, break, continue) can be handled
- References to local variables are resolvable
#![allow(unused)]
fn main() {
let validation = ExtractFunction::new("helper")
.from_file("src/main.rs")
.range(selection)
.validate()?;
match validation {
ValidationResult::Valid => {
println!("Ready to extract");
}
ValidationResult::Warning(msg) => {
println!("Warning: {}", msg);
// e.g., "Selection contains early return - will be converted to Result"
}
ValidationResult::Invalid(msg) => {
println!("Cannot extract: {}", msg);
// e.g., "Selection contains partial expression"
}
}
}
ExtractVariable
Extract an expression into a named variable.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Extract expression at position into a variable
let result = ExtractVariable::new("tax_rate")
.from_file("src/pricing.rs")
.position(Position::new(15, 20)) // Position within expression
.execute()?;
}
Replace All Occurrences
#![allow(unused)]
fn main() {
// Find and replace all identical expressions
ExtractVariable::new("discount_factor")
.from_file("src/pricing.rs")
.position(Position::new(15, 20))
.replace_all_occurrences(true) // Replace all 5 occurrences
.execute()?;
}
Example
#![allow(unused)]
fn main() {
// Before:
fn calculate_price(base: f64, quantity: i32) -> f64 {
let subtotal = base * quantity as f64;
let tax = subtotal * 0.08; // <- position here
let shipping = if subtotal * 0.08 > 10.0 { 0.0 } else { 5.0 };
subtotal + subtotal * 0.08 + shipping
}
// After (with replace_all_occurrences):
fn calculate_price(base: f64, quantity: i32) -> f64 {
let subtotal = base * quantity as f64;
let tax_amount = subtotal * 0.08;
let tax = tax_amount;
let shipping = if tax_amount > 10.0 { 0.0 } else { 5.0 };
subtotal + tax_amount + shipping
}
}
ExtractConstant
Extract a literal value into a named constant.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Extract magic number into a constant
ExtractConstant::new("MAX_RETRIES")
.from_file("src/client.rs")
.position(Position::new(8, 25))
.execute()?;
}
Configuration
#![allow(unused)]
fn main() {
ExtractConstant::new("DEFAULT_TIMEOUT_SECS")
.from_file("src/config.rs")
.position(Position::new(12, 30))
// Place at module level (default) or in impl block
.placement(ConstantPlacement::Module)
// Set visibility
.visibility(Visibility::Crate)
// Replace all occurrences of this literal
.replace_all_occurrences(true)
.execute()?;
}
Example
#![allow(unused)]
fn main() {
// Before:
fn connect() -> Result<Connection> {
let timeout = Duration::from_secs(30); // <- position here
let retries = 3;
for _ in 0..retries {
match try_connect(Duration::from_secs(30)) {
Ok(conn) => return Ok(conn),
Err(_) => sleep(Duration::from_secs(30)),
}
}
Err(Error::Timeout)
}
// After (with replace_all_occurrences):
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn connect() -> Result<Connection> {
let timeout = Duration::from_secs(DEFAULT_TIMEOUT_SECS);
let retries = 3;
for _ in 0..retries {
match try_connect(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
Ok(conn) => return Ok(conn),
Err(_) => sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS)),
}
}
Err(Error::Timeout)
}
}
Language Support
Extract operations support:
| Language | Function | Variable | Constant |
|---|---|---|---|
| Rust | Yes | Yes | Yes |
| TypeScript | Yes | Yes | Yes |
| Python | Yes | Yes | Yes |
| Go | Yes | Yes | Yes |
| Java | Yes | Yes | Yes |
| C# | Yes | Yes | Yes |
| Ruby | Yes | Yes | Limited |
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match ExtractFunction::new("helper").from_file("src/main.rs").range(selection).execute() {
Ok(result) => {
println!("Created: {}", result.function_name);
println!("Diff:\n{}", result.diff());
}
Err(RefactorError::InvalidSelection(msg)) => {
println!("Invalid selection: {}", msg);
}
Err(RefactorError::NameConflict(name)) => {
println!("Name '{}' already exists in scope", name);
}
Err(e) => return Err(e.into()),
}
}
Best Practices
- Use meaningful names - Choose names that describe what the extracted code does
- Start small - Extract simple, self-contained code first
- Preview changes - Always use
.dry_run()before applying - Check parameters - Review inferred parameters for correctness
- Consider visibility - Use the minimum visibility needed
See Also
- Inline - The reverse operation
- Move - Move extracted code to other files
- Scope Analysis - How variables are tracked
Inline Operations
Inline operations replace named entities with their definitions, reversing extraction. This can simplify code by removing unnecessary indirection.
InlineVariable
Replace a variable with its value at all usage sites.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Inline a variable
let result = InlineVariable::new("temp")
.in_file("src/process.rs")
.position(Position::new(12, 8)) // Position of the variable declaration
.execute()?;
println!("Inlined {} usages", result.replacements);
}
Example
#![allow(unused)]
fn main() {
// Before:
fn calculate(x: i32, y: i32) -> i32 {
let sum = x + y; // <- inline this
let doubled = sum * 2;
doubled + sum
}
// After:
fn calculate(x: i32, y: i32) -> i32 {
let doubled = (x + y) * 2;
doubled + (x + y)
}
}
Configuration
#![allow(unused)]
fn main() {
InlineVariable::new("result")
.in_file("src/lib.rs")
.position(Position::new(10, 8))
// Only inline at specific position (not all usages)
.single_usage(Position::new(15, 12))
// Keep the declaration (useful for debugging)
.keep_declaration(true)
// Preview first
.dry_run()
.execute()?;
}
When to Inline Variables
Good candidates for inlining:
- Single-use variables that don’t add clarity
- Trivial expressions like
let x = y; - Variables that make code harder to read
Avoid inlining:
- Variables with meaningful names that document intent
- Complex expressions used multiple times
- Variables that capture expensive computations
InlineFunction
Replace function calls with the function body.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Inline all calls to a function
let result = InlineFunction::new("helper")
.in_file("src/utils.rs")
.all_call_sites(true)
.execute()?;
println!("Inlined {} call sites", result.call_sites_inlined);
}
Single Call Site
#![allow(unused)]
fn main() {
// Inline just one specific call
InlineFunction::new("process")
.in_file("src/main.rs")
.call_site(Position::new(25, 10)) // Position of the call
.execute()?;
}
Example
#![allow(unused)]
fn main() {
// Before:
fn double(x: i32) -> i32 {
x * 2
}
fn calculate(value: i32) -> i32 {
let a = double(value);
let b = double(a);
a + b
}
// After inlining `double`:
fn calculate(value: i32) -> i32 {
let a = value * 2;
let b = a * 2;
a + b
}
}
Configuration
#![allow(unused)]
fn main() {
InlineFunction::new("helper")
.in_file("src/utils.rs")
// Inline all call sites
.all_call_sites(true)
// Also delete the function definition
.delete_definition(true)
// Handle parameter renaming to avoid conflicts
.rename_parameters(true)
// Preview first
.dry_run()
.execute()?;
}
Parameter Substitution
Parameters are substituted with their arguments:
// Before:
fn greet(name: &str, times: i32) {
for _ in 0..times {
println!("Hello, {}!", name);
}
}
fn main() {
greet("World", 3);
}
// After inlining the call:
fn main() {
for _ in 0..3 {
println!("Hello, {}!", "World");
}
}
Handling Complex Cases
The inliner handles:
- Early returns - Converted to conditionals when possible
- Local variables - Renamed to avoid conflicts
- Multiple statements - Wrapped in blocks if needed
- Side effects - Preserved in evaluation order
#![allow(unused)]
fn main() {
// Before:
fn compute(x: i32) -> i32 {
if x < 0 {
return 0; // Early return
}
x * 2
}
fn process(value: i32) -> i32 {
let result = compute(value);
result + 1
}
// After inlining `compute`:
fn process(value: i32) -> i32 {
let result = if value < 0 {
0
} else {
value * 2
};
result + 1
}
}
Validation
Both operations validate before executing:
#![allow(unused)]
fn main() {
let validation = InlineVariable::new("temp")
.in_file("src/main.rs")
.position(Position::new(10, 8))
.validate()?;
match validation {
ValidationResult::Valid => println!("Ready to inline"),
ValidationResult::Warning(msg) => {
println!("Warning: {}", msg);
// e.g., "Variable is used 5 times - consider keeping it"
}
ValidationResult::Invalid(msg) => {
println!("Cannot inline: {}", msg);
// e.g., "Variable has side effects in initialization"
}
}
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match InlineFunction::new("helper").in_file("src/lib.rs").all_call_sites(true).execute() {
Ok(result) => {
println!("Inlined {} calls", result.call_sites_inlined);
}
Err(RefactorError::SymbolNotFound(name)) => {
println!("Function '{}' not found", name);
}
Err(RefactorError::CannotInline(reason)) => {
println!("Cannot inline: {}", reason);
// e.g., "Function is recursive"
// e.g., "Function has multiple return points that cannot be merged"
}
Err(e) => return Err(e.into()),
}
}
Language Support
| Language | Variable | Function |
|---|---|---|
| Rust | Yes | Yes |
| TypeScript | Yes | Yes |
| Python | Yes | Yes |
| Go | Yes | Yes |
| Java | Yes | Yes |
| C# | Yes | Yes |
| Ruby | Yes | Yes |
Best Practices
- Start with single usage - Inline one call site to verify correctness
- Check for side effects - Ensure inlining doesn’t change behavior
- Consider readability - Sometimes the named entity is clearer
- Preview changes - Always use
.dry_run()first - Clean up after - Remove unused definitions
See Also
- Extract - The reverse operation
- Safe Delete - Safely remove unused code
Move Operations
Move operations relocate code between files and modules while maintaining correctness by updating imports and references.
MoveToFile
Move a symbol (function, struct, class, etc.) to a different file.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Move a function to another file
let result = MoveToFile::new("process_data")
.from_file("src/utils.rs")
.to_file("src/processors.rs")
.execute()?;
println!("Moved to: {}", result.target_file.display());
}
Configuration
#![allow(unused)]
fn main() {
MoveToFile::new("DataProcessor")
.from_file("src/lib.rs")
.to_file("src/processing/mod.rs")
// Update all imports across the project
.update_imports(true)
// Add re-export from original location for backwards compatibility
.add_reexport(true)
// Move related items (e.g., impl blocks for a struct)
.include_related(true)
// Preview first
.dry_run()
.execute()?;
}
Example
// Before (src/utils.rs):
pub fn process_data(input: &str) -> Result<Data> {
// ...
}
pub fn other_util() { /* ... */ }
// src/main.rs:
use crate::utils::process_data;
fn main() {
let result = process_data("input");
}
// After moving `process_data` to src/processors.rs:
// src/processors.rs (new or updated):
pub fn process_data(input: &str) -> Result<Data> {
// ...
}
// src/utils.rs:
pub fn other_util() { /* ... */ }
// src/main.rs (imports updated):
use crate::processors::process_data;
fn main() {
let result = process_data("input");
}
Moving with Related Items
When moving a struct, you often want to move its impl blocks too:
#![allow(unused)]
fn main() {
MoveToFile::new("User")
.from_file("src/models.rs")
.to_file("src/user/mod.rs")
.include_related(true) // Moves struct + all impl blocks
.execute()?;
}
MoveBetweenModules
Move code between modules with proper path updates.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Move to a different module path
let result = MoveBetweenModules::new("DataProcessor")
.from_module("crate::utils")
.to_module("crate::processors")
.execute()?;
}
Configuration
#![allow(unused)]
fn main() {
MoveBetweenModules::new("Config")
.from_module("crate::settings")
.to_module("crate::config::types")
// Update all references in the codebase
.update_references(true)
// Add a re-export from the old path
.add_reexport(true)
// Create the target module if it doesn't exist
.create_target_module(true)
.execute()?;
}
Example
#![allow(unused)]
fn main() {
// Before:
// crate::utils::helpers
pub struct DataProcessor { /* ... */ }
// Usage in another file:
use crate::utils::helpers::DataProcessor;
// After moving to crate::processing::core:
// crate::processing::core
pub struct DataProcessor { /* ... */ }
// Usage updated:
use crate::processing::core::DataProcessor;
}
Import Updates
Move operations automatically update imports:
Rust
#![allow(unused)]
fn main() {
// Before:
use crate::utils::process_data;
use crate::utils::{format, validate};
// After moving process_data to crate::processors:
use crate::processors::process_data;
use crate::utils::{format, validate};
}
TypeScript
// Before:
import { processData, format } from './utils';
// After moving processData to ./processors:
import { processData } from './processors';
import { format } from './utils';
Python
# Before:
from utils import process_data, format_data
# After moving process_data to processors:
from processors import process_data
from utils import format_data
Re-exports for Compatibility
To maintain backwards compatibility, add re-exports:
#![allow(unused)]
fn main() {
MoveToFile::new("legacy_api")
.from_file("src/lib.rs")
.to_file("src/legacy/mod.rs")
.add_reexport(true)
.execute()?;
}
This creates:
#![allow(unused)]
fn main() {
// src/lib.rs
pub use crate::legacy::legacy_api; // Re-export for compatibility
// src/legacy/mod.rs
pub fn legacy_api() { /* ... */ }
}
Validation
Move operations validate:
- Target file/module exists or can be created
- No name conflicts at destination
- All references can be updated
- Circular dependencies won’t be introduced
#![allow(unused)]
fn main() {
let validation = MoveToFile::new("helper")
.from_file("src/utils.rs")
.to_file("src/helpers.rs")
.validate()?;
match validation {
ValidationResult::Valid => println!("Ready to move"),
ValidationResult::Warning(msg) => {
println!("Warning: {}", msg);
// e.g., "Target file will be created"
}
ValidationResult::Invalid(msg) => {
println!("Cannot move: {}", msg);
// e.g., "Would create circular dependency"
}
}
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match MoveToFile::new("function").from_file("src/a.rs").to_file("src/b.rs").execute() {
Ok(result) => {
println!("Moved successfully");
println!("Updated {} imports", result.imports_updated);
}
Err(RefactorError::SymbolNotFound(name)) => {
println!("Symbol '{}' not found in source file", name);
}
Err(RefactorError::NameConflict(name)) => {
println!("'{}' already exists in target file", name);
}
Err(RefactorError::CircularDependency(msg)) => {
println!("Would create circular dependency: {}", msg);
}
Err(e) => return Err(e.into()),
}
}
Language Support
| Language | MoveToFile | MoveBetweenModules |
|---|---|---|
| Rust | Yes | Yes |
| TypeScript | Yes | Yes |
| Python | Yes | Yes |
| Go | Yes | Yes |
| Java | Yes | Yes |
| C# | Yes | Yes |
| Ruby | Yes | Limited |
Best Practices
- Preview changes - Always use
.dry_run()first - Add re-exports initially - Remove them after migration period
- Move related items together - Use
.include_related(true) - Update tests - Move operations update source files, check test imports
- Commit before moving - Have a clean git state to easily revert
See Also
- Change Signature - Modify function signatures
- Safe Delete - Remove unused code after moving
Change Signature
Change signature operations modify function signatures while automatically updating all call sites to match.
Overview
ChangeSignature allows you to:
- Add, remove, or rename parameters
- Reorder parameters
- Change return types
- Add default values for new parameters
All call sites are updated automatically to maintain correctness.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Add a new parameter with a default value
let result = ChangeSignature::for_function("process")
.in_file("src/lib.rs")
.add_parameter("timeout", "Duration", "Duration::from_secs(30)")
.execute()?;
println!("Updated {} call sites", result.call_sites_updated);
}
Adding Parameters
With Default Value
#![allow(unused)]
fn main() {
ChangeSignature::for_function("connect")
.in_file("src/client.rs")
.add_parameter("retry_count", "u32", "3")
.execute()?;
}
Before:
#![allow(unused)]
fn main() {
fn connect(host: &str) -> Result<Connection> { /* ... */ }
// Call sites:
let conn = connect("localhost")?;
}
After:
#![allow(unused)]
fn main() {
fn connect(host: &str, retry_count: u32) -> Result<Connection> { /* ... */ }
// Call sites updated:
let conn = connect("localhost", 3)?;
}
At Specific Position
#![allow(unused)]
fn main() {
ChangeSignature::for_function("create_user")
.in_file("src/users.rs")
.add_parameter_at("email", "String", 1) // After first param
.default_value("String::new()")
.execute()?;
}
Multiple Parameters
#![allow(unused)]
fn main() {
ChangeSignature::for_function("configure")
.in_file("src/config.rs")
.add_parameter("debug", "bool", "false")
.add_parameter("verbose", "bool", "false")
.add_parameter("timeout", "Duration", "Duration::from_secs(60)")
.execute()?;
}
Removing Parameters
#![allow(unused)]
fn main() {
ChangeSignature::for_function("old_api")
.in_file("src/legacy.rs")
.remove_parameter("unused_flag")
.execute()?;
}
Before:
#![allow(unused)]
fn main() {
fn old_api(data: &str, unused_flag: bool) -> String { /* ... */ }
// Call sites:
let result = old_api("input", true);
let result = old_api("other", false);
}
After:
#![allow(unused)]
fn main() {
fn old_api(data: &str) -> String { /* ... */ }
// Call sites updated:
let result = old_api("input");
let result = old_api("other");
}
Renaming Parameters
#![allow(unused)]
fn main() {
ChangeSignature::for_function("process")
.in_file("src/processor.rs")
.rename_parameter("input", "source")
.execute()?;
}
This updates:
- The parameter name in the function definition
- All uses of the parameter within the function body
- Named arguments at call sites (for languages that support them)
Reordering Parameters
#![allow(unused)]
fn main() {
ChangeSignature::for_function("create_user")
.in_file("src/users.rs")
.reorder_parameters(&["name", "email", "role"])
.execute()?;
}
Before:
#![allow(unused)]
fn main() {
fn create_user(role: Role, name: String, email: String) -> User { /* ... */ }
// Call sites:
create_user(Role::Admin, "Alice".into(), "alice@example.com".into())
}
After:
#![allow(unused)]
fn main() {
fn create_user(name: String, email: String, role: Role) -> User { /* ... */ }
// Call sites reordered:
create_user("Alice".into(), "alice@example.com".into(), Role::Admin)
}
Changing Types
#![allow(unused)]
fn main() {
ChangeSignature::for_function("process")
.in_file("src/lib.rs")
.change_parameter_type("count", "u32", "usize")
.execute()?;
}
Note: Type changes may require manual updates if the new type is incompatible with existing usage.
Complex Changes
Combine multiple modifications:
#![allow(unused)]
fn main() {
ChangeSignature::for_function("legacy_handler")
.in_file("src/handlers.rs")
// Remove deprecated parameters
.remove_parameter("deprecated_flag")
.remove_parameter("old_config")
// Add new parameters
.add_parameter("options", "Options", "Options::default()")
// Rename for clarity
.rename_parameter("cb", "callback")
// Reorder for consistency
.reorder_parameters(&["request", "options", "callback"])
.execute()?;
}
Method Signatures
For methods on structs/classes:
#![allow(unused)]
fn main() {
// Rust
ChangeSignature::for_method("process")
.on_type("DataProcessor")
.in_file("src/processor.rs")
.add_parameter("config", "&Config", "&Config::default()")
.execute()?;
// Java
ChangeSignature::for_method("process")
.on_type("DataProcessor")
.in_file("src/DataProcessor.java")
.add_parameter("config", "Config", "new Config()")
.execute()?;
}
Validation
#![allow(unused)]
fn main() {
let validation = ChangeSignature::for_function("api")
.in_file("src/lib.rs")
.add_parameter("new_param", "String", "String::new()")
.validate()?;
match validation {
ValidationResult::Valid => println!("Ready to change signature"),
ValidationResult::Warning(msg) => {
println!("Warning: {}", msg);
// e.g., "Found 50 call sites to update"
}
ValidationResult::Invalid(msg) => {
println!("Cannot change signature: {}", msg);
// e.g., "Function is part of a trait implementation"
}
}
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match ChangeSignature::for_function("api").in_file("src/lib.rs")
.add_parameter("param", "Type", "default")
.execute()
{
Ok(result) => {
println!("Updated {} call sites", result.call_sites_updated);
}
Err(RefactorError::SymbolNotFound(name)) => {
println!("Function '{}' not found", name);
}
Err(RefactorError::TraitConstraint(msg)) => {
println!("Cannot change: {}", msg);
// Signature must match trait definition
}
Err(RefactorError::AmbiguousReference(msg)) => {
println!("Ambiguous function: {}", msg);
// Multiple functions with same name
}
Err(e) => return Err(e.into()),
}
}
Language-Specific Notes
Rust
- Trait implementations require matching trait changes
- Generic parameters are preserved
- Lifetime annotations are maintained
TypeScript
- Optional parameters use
?syntax - Destructured parameters are handled
- Overloads need manual attention
Python
- Default values must be valid Python expressions
*argsand**kwargsare preserved- Decorators are maintained
Go
- Named return values are supported
- Multiple return values are handled
- Interface implementations need matching changes
Best Practices
- Preview changes - Always use
.dry_run()first - Start with additions - Add parameters before removing old ones
- Provide meaningful defaults - Help callers migrate gradually
- Update tests - Test call sites may need attention
- Consider deprecation - Mark old signature as deprecated first
See Also
- Safe Delete - Remove unused parameters
- Extract - Create functions with proper signatures
Safe Delete
Safe delete operations remove code while checking for usages, preventing accidental breakage.
Overview
Unlike regular deletion, SafeDelete:
- Checks for usages - Warns if the symbol is still referenced
- Shows affected locations - Lists all files that would break
- Supports cascade - Optionally delete dependent code
- Provides alternatives - Suggests refactoring options
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Delete a function, checking for usages first
let result = SafeDelete::symbol("unused_helper")
.in_file("src/utils.rs")
.check_usages(true)
.execute()?;
match result {
DeleteResult::Deleted => println!("Successfully deleted"),
DeleteResult::HasUsages(usages) => {
println!("Cannot delete: {} usages found", usages.len());
for usage in usages {
println!(" - {}:{}", usage.file.display(), usage.line);
}
}
}
}
Delete Types
Functions
#![allow(unused)]
fn main() {
SafeDelete::function("helper_function")
.in_file("src/utils.rs")
.execute()?;
}
Types (Struct, Class, Enum)
#![allow(unused)]
fn main() {
SafeDelete::type_def("OldConfig")
.in_file("src/config.rs")
.execute()?;
}
Methods
#![allow(unused)]
fn main() {
SafeDelete::method("deprecated_method")
.on_type("MyStruct")
.in_file("src/lib.rs")
.execute()?;
}
Variables/Constants
#![allow(unused)]
fn main() {
SafeDelete::constant("UNUSED_CONST")
.in_file("src/constants.rs")
.execute()?;
}
Imports
#![allow(unused)]
fn main() {
SafeDelete::import("unused_module")
.in_file("src/main.rs")
.execute()?;
}
Cascade Delete
Delete a symbol and all code that depends on it:
#![allow(unused)]
fn main() {
SafeDelete::symbol("deprecated_module")
.in_file("src/lib.rs")
.cascade(true)
.execute()?;
}
Warning: Cascade delete can remove significant amounts of code. Always preview first!
Cascade Example
#![allow(unused)]
fn main() {
// If we delete `HelperTrait`:
pub trait HelperTrait {
fn help(&self);
}
impl HelperTrait for MyStruct { // Would also be deleted
fn help(&self) { /* ... */ }
}
fn use_helper<T: HelperTrait>(t: T) { // Would also be deleted
t.help();
}
}
Preview Cascade
See what would be deleted without doing it:
#![allow(unused)]
fn main() {
let preview = SafeDelete::symbol("deprecated_api")
.in_file("src/lib.rs")
.cascade(true)
.preview()?;
println!("Would delete {} items:", preview.items.len());
for item in &preview.items {
println!(" - {} ({}) at {}:{}",
item.name,
item.kind,
item.file.display(),
item.line);
}
// Optionally proceed
if user_confirmed() {
SafeDelete::symbol("deprecated_api")
.in_file("src/lib.rs")
.cascade(true)
.execute()?;
}
}
Force Delete
Delete even if there are usages (use with caution):
#![allow(unused)]
fn main() {
SafeDelete::symbol("must_remove")
.in_file("src/lib.rs")
.force(true) // Delete despite usages
.execute()?;
}
This will delete the symbol and leave broken references. Use this only when you plan to fix references manually.
Search Scope
Control where to search for usages:
#![allow(unused)]
fn main() {
SafeDelete::symbol("internal_helper")
.in_file("src/utils.rs")
// Only search in specific directory
.search_scope("src/internal")
.execute()?;
// Or exclude certain paths
SafeDelete::symbol("helper")
.in_file("src/lib.rs")
.exclude_from_search("**/tests/**")
.exclude_from_search("**/examples/**")
.execute()?;
}
Handling Results
#![allow(unused)]
fn main() {
let result = SafeDelete::symbol("maybe_used")
.in_file("src/lib.rs")
.check_usages(true)
.execute()?;
match result {
DeleteResult::Deleted => {
println!("Symbol deleted successfully");
}
DeleteResult::HasUsages(usages) => {
println!("Found {} usages:", usages.len());
for usage in usages {
println!(" {}:{}:{} - {}",
usage.file.display(),
usage.line,
usage.column,
usage.context); // Surrounding code
}
// Options:
// 1. Fix usages manually, then delete
// 2. Use cascade to delete dependent code
// 3. Force delete and fix later
}
DeleteResult::NotFound => {
println!("Symbol not found");
}
}
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match SafeDelete::symbol("item").in_file("src/lib.rs").execute() {
Ok(DeleteResult::Deleted) => println!("Success"),
Ok(DeleteResult::HasUsages(usages)) => {
println!("Has {} usages", usages.len());
}
Ok(DeleteResult::NotFound) => {
println!("Symbol not found");
}
Err(RefactorError::SymbolNotFound(name)) => {
println!("File doesn't contain '{}'", name);
}
Err(RefactorError::AmbiguousReference(msg)) => {
println!("Multiple symbols match: {}", msg);
}
Err(e) => return Err(e.into()),
}
}
Language Support
| Language | Function | Type | Method | Variable | Import |
|---|---|---|---|---|---|
| Rust | Yes | Yes | Yes | Yes | Yes |
| TypeScript | Yes | Yes | Yes | Yes | Yes |
| Python | Yes | Yes | Yes | Yes | Yes |
| Go | Yes | Yes | Yes | Yes | Yes |
| Java | Yes | Yes | Yes | Yes | Yes |
| C# | Yes | Yes | Yes | Yes | Yes |
| Ruby | Yes | Yes | Yes | Yes | Yes |
Integration with Other Operations
After Inline
#![allow(unused)]
fn main() {
// Inline a function, then safely delete it
InlineFunction::new("helper")
.in_file("src/utils.rs")
.all_call_sites(true)
.execute()?;
SafeDelete::function("helper")
.in_file("src/utils.rs")
.execute()?; // Should have no usages now
}
After Move
#![allow(unused)]
fn main() {
// Move a function, then delete the old location's re-export
MoveToFile::new("process")
.from_file("src/old.rs")
.to_file("src/new.rs")
.add_reexport(false) // Don't add re-export
.execute()?;
// Or if re-export was added initially, remove it later
SafeDelete::import("process")
.in_file("src/old.rs")
.execute()?;
}
Best Practices
- Always preview first - Use
.check_usages(true)before deletion - Prefer cascade sparingly - Review what will be deleted
- Check tests - Tests may have usages not in main source
- Consider deprecation first - Mark as deprecated before deleting
- Keep git clean - Easy to revert if something breaks
See Also
- Find Dead Code - Identify unused code to delete
- Inline - Inline before deleting
Find Dead Code
Dead code analysis identifies unused code that can be safely removed, helping keep your codebase clean and maintainable.
Overview
FindDeadCode detects:
- Unused functions - Functions never called
- Unused types - Structs, classes, enums with no references
- Unused imports - Import statements for unused items
- Unused variables - Variables assigned but never read
- Unreachable code - Code after unconditional returns
- Unused parameters - Function parameters never used
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find all dead code in a workspace
let report = FindDeadCode::in_workspace("./project")
.execute()?;
println!("Found {} dead code items:", report.items.len());
for item in &report.items {
println!(" {} ({}) at {}:{}",
item.name,
item.kind,
item.file.display(),
item.line);
}
}
Dead Code Types
Filter by Type
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
// Select specific types to find
.include(DeadCodeType::UnusedFunctions)
.include(DeadCodeType::UnusedImports)
.include(DeadCodeType::UnusedVariables)
.execute()?;
}
All Dead Code Types
#![allow(unused)]
fn main() {
pub enum DeadCodeType {
UnusedFunctions, // Functions never called
UnusedTypes, // Structs/classes/enums not referenced
UnusedImports, // Imported but unused items
UnusedVariables, // Assigned but never read
UnusedParameters, // Function params never used in body
UnreachableCode, // Code after return/break/continue
UnusedFields, // Struct fields never accessed
UnusedConstants, // Constants never referenced
}
}
Example Output
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
.include(DeadCodeType::UnusedFunctions)
.execute()?;
// Report contents:
// DeadCodeItem { name: "old_helper", kind: UnusedFunction, file: "src/utils.rs", line: 45 }
// DeadCodeItem { name: "deprecated_api", kind: UnusedFunction, file: "src/lib.rs", line: 123 }
}
Scope Control
Specific File
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_file("src/utils.rs")
.execute()?;
}
Specific Directory
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_directory("src/legacy")
.execute()?;
}
With Exclusions
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
// Exclude test files
.exclude("**/tests/**")
.exclude("**/*_test.rs")
// Exclude generated code
.exclude("**/generated/**")
// Exclude examples
.exclude("**/examples/**")
.execute()?;
}
Analysis Depth
Cross-File Analysis
By default, analysis considers usages across all files:
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
.cross_file_analysis(true) // Default
.execute()?;
}
Single-File Analysis
Faster but may report false positives:
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_file("src/utils.rs")
.cross_file_analysis(false)
.execute()?;
}
Export Awareness
Control how exports are treated:
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
// Treat all exports as potentially used (library mode)
.consider_exports_used(true)
.execute()?;
// Or for applications, exports without usages are dead:
let report = FindDeadCode::in_workspace("./project")
.consider_exports_used(false)
.execute()?;
}
Report Formats
Text Summary
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project").execute()?;
println!("{}", report.summary());
// Output:
// Dead Code Report
// ================
// Unused functions: 5
// Unused imports: 12
// Unused variables: 3
// Total: 20 items
}
Detailed Report
#![allow(unused)]
fn main() {
for item in &report.items {
println!("{}: {} at {}:{}",
item.kind,
item.name,
item.file.display(),
item.line);
if let Some(context) = &item.context {
println!(" {}", context); // Surrounding code
}
if let Some(reason) = &item.reason {
println!(" Reason: {}", reason); // Why it's considered dead
}
}
}
JSON Export
#![allow(unused)]
fn main() {
let json = report.to_json()?;
std::fs::write("dead-code-report.json", json)?;
}
SARIF Format (for CI/CD)
#![allow(unused)]
fn main() {
let sarif = report.to_sarif()?;
std::fs::write("dead-code.sarif", sarif)?;
}
Integration with Safe Delete
Automatically clean up dead code:
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
.include(DeadCodeType::UnusedFunctions)
.execute()?;
// Preview what would be deleted
for item in &report.items {
println!("Would delete: {} in {}", item.name, item.file.display());
}
// Delete all dead code (use with caution!)
if user_confirmed() {
for item in report.items {
SafeDelete::symbol(&item.name)
.in_file(&item.file)
.force(true) // We know it's unused
.execute()?;
}
}
}
Language-Specific Considerations
Rust
- Respects
#[allow(dead_code)]annotations - Considers trait implementations as used
- Handles conditional compilation (
#[cfg(...)])
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
.respect_annotations(true) // Skip #[allow(dead_code)]
.execute()?;
}
TypeScript
- Considers module exports
- Handles type-only imports
- Respects
// @ts-ignorecomments
Python
- Handles
__all__exports - Considers
if __name__ == "__main__"blocks - Respects
# noqacomments
Go
- Considers exported (capitalized) identifiers
- Handles
init()functions - Respects
//nolintcomments
False Positive Handling
Some code may appear unused but is actually needed:
#![allow(unused)]
fn main() {
let report = FindDeadCode::in_workspace("./project")
// Skip symbols matching patterns
.skip_pattern("test_*")
.skip_pattern("*_benchmark")
// Skip specific files
.exclude("**/fixtures/**")
// Skip items with specific annotations
.respect_annotations(true)
.execute()?;
}
Common False Positives
- Reflection/runtime usage - Code used via strings/macros
- FFI exports - Functions called from C/other languages
- Framework callbacks - Methods called by framework magic
- Test fixtures - Code only used in tests
CI/CD Integration
GitHub Actions
- name: Check for dead code
run: |
refactor dead-code --format sarif > dead-code.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: dead-code.sarif
Pre-commit Hook
#!/bin/bash
result=$(refactor dead-code --exit-code)
if [ $? -ne 0 ]; then
echo "Dead code found! Please remove unused code."
exit 1
fi
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match FindDeadCode::in_workspace("./project").execute() {
Ok(report) => {
println!("Found {} dead items", report.items.len());
}
Err(RefactorError::ParseError { path, message }) => {
println!("Failed to parse {}: {}", path.display(), message);
}
Err(RefactorError::IoError(e)) => {
println!("IO error: {}", e);
}
Err(e) => return Err(e.into()),
}
}
Language Support
| Language | Functions | Types | Imports | Variables | Parameters |
|---|---|---|---|---|---|
| Rust | Yes | Yes | Yes | Yes | Yes |
| TypeScript | Yes | Yes | Yes | Yes | Yes |
| Python | Yes | Yes | Yes | Yes | Yes |
| Go | Yes | Yes | Yes | Yes | Yes |
| Java | Yes | Yes | Yes | Yes | Yes |
| C# | Yes | Yes | Yes | Yes | Yes |
| Ruby | Yes | Yes | Yes | Yes | Limited |
Best Practices
- Run regularly - Include in CI/CD pipeline
- Start conservative - Use
consider_exports_used(true)for libraries - Review before deleting - Some “dead” code may be needed
- Exclude tests initially - Test utilities may seem unused
- Handle false positives - Use annotations or skip patterns
See Also
- Safe Delete - Remove dead code safely
- Scope Analysis - How usages are tracked
Enhanced Repository Discovery
Refactor DSL provides advanced repository discovery and filtering capabilities, enabling you to target specific repositories in large organizations based on dependencies, frameworks, metrics, and languages.
Overview
The discovery module extends basic Git repository filtering with:
- Dependency filtering - Find repos using specific packages
- Framework detection - Identify repos using React, Rails, Spring, etc.
- Metrics filtering - Filter by lines of code, file count, complexity
- Language detection - Target repos by primary programming language
Quick Start
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find all React 17+ projects with over 1000 lines of code
Codemod::from_github_org("acme-corp", token)
.repositories(|r| r
.has_dependency("react", ">=17.0")
.uses_framework(Framework::React)
.lines_of_code(ComparisonOp::GreaterThan, 1000.0))
.apply(upgrade_operation)
.execute()?;
}
Filter Types
Dependency Filters
Filter by package dependencies:
#![allow(unused)]
fn main() {
.repositories(|r| r
// NPM packages
.has_dependency("react", ">=17.0")
.has_dependency("typescript", "*")
// Cargo crates
.has_dependency("tokio", ">=1.0")
// Python packages
.has_dependency("django", ">=3.0")
)
}
Framework Filters
Filter by detected frameworks:
#![allow(unused)]
fn main() {
.repositories(|r| r
.uses_framework(Framework::NextJs)
// or
.uses_framework(Framework::Rails)
// or
.uses_framework(Framework::Spring)
)
}
Metrics Filters
Filter by code metrics:
#![allow(unused)]
fn main() {
.repositories(|r| r
.lines_of_code(ComparisonOp::GreaterThan, 1000.0)
.file_count(ComparisonOp::LessThan, 100.0)
.complexity(ComparisonOp::LessThan, 20.0)
)
}
Language Filters
Filter by primary language:
#![allow(unused)]
fn main() {
.repositories(|r| r
.primary_language("rust")
// Or by percentage
.language_percentage("typescript", ComparisonOp::GreaterThan, 50.0)
)
}
Combining Filters
Filters combine with AND logic:
#![allow(unused)]
fn main() {
Codemod::from_github_org("company", token)
.repositories(|r| r
// All conditions must be true
.primary_language("typescript")
.uses_framework(Framework::React)
.has_dependency("react", ">=18.0")
.lines_of_code(ComparisonOp::GreaterThan, 5000.0)
)
.apply(migration)
.execute()?;
}
For OR logic, use multiple discovery passes:
#![allow(unused)]
fn main() {
// Find repos using either React OR Vue
let react_repos = discover_repos()
.uses_framework(Framework::React)
.collect()?;
let vue_repos = discover_repos()
.uses_framework(Framework::Vue)
.collect()?;
let all_frontend_repos: HashSet<_> = react_repos.union(&vue_repos).collect();
}
Discovery Sources
GitHub Organization
#![allow(unused)]
fn main() {
Codemod::from_github_org("organization-name", github_token)
.repositories(|r| r.has_file("Cargo.toml"))
.apply(transform)
.execute()?;
}
GitHub User
#![allow(unused)]
fn main() {
Codemod::from_github_user("username", github_token)
.repositories(|r| r.primary_language("rust"))
.apply(transform)
.execute()?;
}
Local Directory
#![allow(unused)]
fn main() {
Codemod::from_directory("./workspace")
.repositories(|r| r.uses_framework(Framework::Django))
.apply(transform)
.execute()?;
}
Custom Sources
#![allow(unused)]
fn main() {
let repos = vec![
PathBuf::from("./project-a"),
PathBuf::from("./project-b"),
PathBuf::from("./project-c"),
];
Codemod::from_paths(repos)
.repositories(|r| r.has_dependency("lodash", "*"))
.apply(transform)
.execute()?;
}
Advanced Repository Filter
Use AdvancedRepoFilter for complex filtering:
#![allow(unused)]
fn main() {
use refactor::discovery::AdvancedRepoFilter;
let filter = AdvancedRepoFilter::new()
// Base Git filters
.branch("main")
.has_file("package.json")
.recent_commits(30)
// Dependency filters
.dependency(DependencyFilter::npm("react", ">=17"))
.dependency(DependencyFilter::npm("typescript", "*"))
// Framework detection
.framework(FrameworkFilter::new(Framework::NextJs))
// Metrics
.metric(MetricFilter::lines_of_code(ComparisonOp::GreaterThan, 1000.0))
// Language
.language(LanguageFilter::primary("typescript"));
// Apply filter
let matching_repos = filter.discover("./workspace")?;
}
Caching and Performance
Discovery operations cache results for performance:
#![allow(unused)]
fn main() {
Codemod::from_github_org("large-org", token)
.repositories(|r| r
.cache_duration(Duration::from_secs(3600)) // Cache for 1 hour
.parallel_discovery(true) // Parallel analysis
.has_dependency("react", "*"))
.apply(transform)
.execute()?;
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match Codemod::from_github_org("org", token)
.repositories(|r| r.has_dependency("react", "*"))
.apply(transform)
.execute()
{
Ok(results) => {
println!("Processed {} repositories", results.len());
}
Err(RefactorError::GithubApiError(msg)) => {
println!("GitHub API error: {}", msg);
}
Err(RefactorError::RateLimited(retry_after)) => {
println!("Rate limited. Retry after {} seconds", retry_after);
}
Err(e) => return Err(e.into()),
}
}
Use Cases
Organization-Wide Dependency Updates
#![allow(unused)]
fn main() {
// Update lodash to v4.17.21 across all JS projects
Codemod::from_github_org("company", token)
.repositories(|r| r
.has_dependency("lodash", "<4.17.21"))
.apply(|ctx| {
ctx.update_dependency("lodash", "4.17.21")
})
.create_prs(true)
.execute()?;
}
Framework Migration
#![allow(unused)]
fn main() {
// Find all Create React App projects for Next.js migration
Codemod::from_github_org("company", token)
.repositories(|r| r
.has_dependency("react-scripts", "*")
.lines_of_code(ComparisonOp::LessThan, 10000.0)) // Small projects first
.collect_repos()?;
}
Security Auditing
#![allow(unused)]
fn main() {
// Find repos with vulnerable dependency versions
Codemod::from_github_org("company", token)
.repositories(|r| r
.has_dependency("log4j", "<2.17.0")) // Vulnerable version
.apply(|ctx| {
ctx.create_security_issue()
})
.execute()?;
}
Language Standardization
#![allow(unused)]
fn main() {
// Find all TypeScript projects not using strict mode
Codemod::from_github_org("company", token)
.repositories(|r| r
.primary_language("typescript")
.has_file("tsconfig.json"))
.apply(|ctx| {
ctx.ensure_strict_mode()
})
.execute()?;
}
See Also
- Dependency Filters - Package dependency filtering
- Framework Filters - Framework detection
- Metrics Filters - Code metrics filtering
- Language Filters - Language-based filtering
- Multi-Repository - Basic multi-repo operations
Dependency Filters
Filter repositories based on their package dependencies across multiple ecosystems.
Overview
DependencyFilter analyzes package manifest files to find repositories using specific dependencies:
- npm/yarn -
package.json - Cargo -
Cargo.toml - pip/Poetry -
requirements.txt,pyproject.toml,setup.py - Maven -
pom.xml - Gradle -
build.gradle,build.gradle.kts - Bundler -
Gemfile - Go modules -
go.mod - NuGet -
*.csproj,packages.config
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find repos using a specific dependency
Codemod::from_github_org("company", token)
.repositories(|r| r
.has_dependency("react", ">=17.0"))
.apply(transform)
.execute()?;
}
Version Specifiers
Exact Version
#![allow(unused)]
fn main() {
.has_dependency("lodash", "4.17.21")
}
Range
#![allow(unused)]
fn main() {
// Greater than or equal
.has_dependency("react", ">=17.0")
// Less than
.has_dependency("vulnerable-pkg", "<2.0")
// Between
.has_dependency("typescript", ">=4.0,<5.0")
}
Any Version
#![allow(unused)]
fn main() {
.has_dependency("express", "*")
}
Semantic Versioning
#![allow(unused)]
fn main() {
// Major version
.has_dependency("react", "^17.0") // >=17.0.0, <18.0.0
// Minor version
.has_dependency("axios", "~0.21") // >=0.21.0, <0.22.0
}
Ecosystem-Specific Filters
NPM/Yarn
#![allow(unused)]
fn main() {
use refactor::discovery::DependencyFilter;
// Production dependency
DependencyFilter::npm("react", ">=17.0")
// Dev dependency only
DependencyFilter::npm_dev("typescript", ">=4.0")
// Peer dependency
DependencyFilter::npm_peer("react", ">=16.8")
}
Cargo (Rust)
#![allow(unused)]
fn main() {
// Regular dependency
DependencyFilter::cargo("tokio", ">=1.0")
// Dev dependency
DependencyFilter::cargo_dev("mockall", "*")
// Build dependency
DependencyFilter::cargo_build("cc", "*")
}
Python (pip/Poetry)
#![allow(unused)]
fn main() {
// Any Python package
DependencyFilter::pip("django", ">=3.0")
// Poetry-specific
DependencyFilter::poetry("fastapi", "*")
// From requirements.txt
DependencyFilter::requirements("numpy", ">=1.20")
}
Maven/Gradle (Java)
#![allow(unused)]
fn main() {
// Maven dependency
DependencyFilter::maven("org.springframework:spring-boot", ">=2.5")
// Gradle
DependencyFilter::gradle("com.google.guava:guava", "*")
}
Bundler (Ruby)
#![allow(unused)]
fn main() {
DependencyFilter::bundler("rails", ">=6.0")
DependencyFilter::bundler("rspec", "*")
}
Go Modules
#![allow(unused)]
fn main() {
DependencyFilter::go("github.com/gin-gonic/gin", ">=1.7")
}
NuGet (C#)
#![allow(unused)]
fn main() {
DependencyFilter::nuget("Newtonsoft.Json", ">=13.0")
}
Multiple Dependencies
All Required (AND)
#![allow(unused)]
fn main() {
.repositories(|r| r
.has_dependency("react", ">=17.0")
.has_dependency("react-dom", ">=17.0")
.has_dependency("typescript", ">=4.0"))
}
Any Required (OR)
Use separate filters and combine:
#![allow(unused)]
fn main() {
let has_react = DependencyFilter::npm("react", "*");
let has_vue = DependencyFilter::npm("vue", "*");
// Find repos with either
let repos = discover_repos("./workspace")?
.filter(|repo| has_react.matches(repo) || has_vue.matches(repo))
.collect();
}
Negative Filters
Find repos that don’t have a dependency:
#![allow(unused)]
fn main() {
.repositories(|r| r
.missing_dependency("moment") // Repos without moment.js
)
}
Transitive Dependencies
By default, only direct dependencies are checked. Enable transitive:
#![allow(unused)]
fn main() {
DependencyFilter::npm("lodash", "*")
.include_transitive(true)
}
Note: Transitive analysis requires package-lock.json, Cargo.lock, etc.
Direct Usage
#![allow(unused)]
fn main() {
use refactor::discovery::DependencyFilter;
let filter = DependencyFilter::npm("react", ">=17.0");
// Check a single repository
if filter.matches(Path::new("./my-project"))? {
println!("Project uses React 17+");
}
// Get dependency info
if let Some(info) = filter.get_dependency_info(Path::new("./my-project"))? {
println!("Found: {} v{}", info.name, info.version);
println!("Type: {:?}", info.dependency_type); // Production, Dev, Peer
}
}
Parsing Examples
package.json
{
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"typescript": "^4.5.0"
}
}
#![allow(unused)]
fn main() {
.has_dependency("react", ">=17.0") // Matches ^17.0.2
.has_dependency("typescript", ">=4.5") // Matches ^4.5.0 in devDeps
}
Cargo.toml
[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = "1.0"
[dev-dependencies]
mockall = "0.11"
#![allow(unused)]
fn main() {
.has_dependency("tokio", ">=1.0") // Matches
.has_dependency("serde", ">=1.0") // Matches
}
requirements.txt
Django>=3.2,<4.0
numpy==1.21.0
requests
#![allow(unused)]
fn main() {
.has_dependency("django", ">=3.2") // Matches
.has_dependency("numpy", "1.21.0") // Matches exact
.has_dependency("requests", "*") // Matches (no version = any)
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
let filter = DependencyFilter::npm("react", ">=17.0");
match filter.matches(Path::new("./project")) {
Ok(true) => println!("Matches"),
Ok(false) => println!("Doesn't match"),
Err(RefactorError::ManifestNotFound(path)) => {
println!("No package.json found at {}", path.display());
}
Err(RefactorError::ParseError { path, message }) => {
println!("Failed to parse {}: {}", path.display(), message);
}
Err(e) => return Err(e.into()),
}
}
Performance Tips
- Specify ecosystem - Use
npm(),cargo(), etc. instead of generichas_dependency() - Avoid transitive - Only enable when necessary
- Cache results - Discovery caches manifest parsing
See Also
- Framework Filters - Higher-level framework detection
- Enhanced Discovery - Full discovery guide
Framework Filters
Detect and filter repositories by the frameworks they use, going beyond simple dependency checks.
Overview
FrameworkFilter identifies frameworks through multiple signals:
- Configuration files (e.g.,
next.config.js,angular.json) - Dependency patterns (combinations of packages)
- Project structure (directories, file patterns)
- Import patterns in source code
Supported Frameworks
JavaScript/TypeScript
| Framework | Detection Method |
|---|---|
| React | react dependency + JSX files |
| Next.js | next.config.* + next dependency |
| Vue | vue dependency + .vue files |
| Nuxt | nuxt.config.* + nuxt dependency |
| Angular | angular.json + @angular/core |
| Svelte | svelte.config.* + svelte dependency |
| Express | express + server patterns |
| NestJS | @nestjs/core + decorators |
| Gatsby | gatsby-config.* + gatsby |
| Remix | remix.config.* + @remix-run/* |
Python
| Framework | Detection Method |
|---|---|
| Django | django + manage.py + settings.py |
| Flask | flask + app.py patterns |
| FastAPI | fastapi + async patterns |
| Pyramid | pyramid dependency |
| Tornado | tornado dependency |
Ruby
| Framework | Detection Method |
|---|---|
| Rails | rails gem + config/application.rb |
| Sinatra | sinatra gem + app patterns |
| Hanami | hanami gem |
Java
| Framework | Detection Method |
|---|---|
| Spring Boot | spring-boot-* + @SpringBootApplication |
| Spring | spring-* dependencies |
| Quarkus | quarkus-* + application.properties |
| Micronaut | micronaut-* |
| Jakarta EE | jakarta.* dependencies |
Go
| Framework | Detection Method |
|---|---|
| Gin | github.com/gin-gonic/gin |
| Echo | github.com/labstack/echo |
| Fiber | github.com/gofiber/fiber |
| Chi | github.com/go-chi/chi |
Rust
| Framework | Detection Method |
|---|---|
| Actix | actix-web crate |
| Axum | axum crate |
| Rocket | rocket crate |
| Warp | warp crate |
C#
| Framework | Detection Method |
|---|---|
| ASP.NET Core | Microsoft.AspNetCore.* |
| Blazor | Microsoft.AspNetCore.Components.* |
| WPF | PresentationFramework reference |
| WinForms | System.Windows.Forms reference |
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find all Next.js projects
Codemod::from_github_org("company", token)
.repositories(|r| r
.uses_framework(Framework::NextJs))
.apply(transform)
.execute()?;
}
Framework Enum
#![allow(unused)]
fn main() {
pub enum Framework {
// JavaScript/TypeScript
React,
NextJs,
Vue,
Nuxt,
Angular,
Svelte,
Express,
NestJS,
Gatsby,
Remix,
// Python
Django,
Flask,
FastAPI,
// Ruby
Rails,
Sinatra,
// Java
Spring,
SpringBoot,
Quarkus,
// Go
Gin,
Echo,
Fiber,
// Rust
Actix,
Axum,
Rocket,
// C#
AspNetCore,
Blazor,
}
}
Multiple Frameworks
All Required (AND)
#![allow(unused)]
fn main() {
.repositories(|r| r
.uses_framework(Framework::React)
.uses_framework(Framework::Express)) // Full-stack React + Express
}
Any Required (OR)
#![allow(unused)]
fn main() {
// Find repos using any React meta-framework
.repositories(|r| r
.uses_any_framework(&[
Framework::NextJs,
Framework::Gatsby,
Framework::Remix,
]))
}
Framework Detection Details
Next.js Detection
#![allow(unused)]
fn main() {
// Detected by:
// 1. next.config.js or next.config.mjs exists
// 2. package.json has "next" dependency
// 3. pages/ or app/ directory structure
// 4. Imports from "next/*"
.uses_framework(Framework::NextJs)
}
Rails Detection
#![allow(unused)]
fn main() {
// Detected by:
// 1. Gemfile contains "rails"
// 2. config/application.rb exists
// 3. app/controllers/ structure
// 4. config/routes.rb exists
.uses_framework(Framework::Rails)
}
Spring Boot Detection
#![allow(unused)]
fn main() {
// Detected by:
// 1. spring-boot-starter-* dependencies
// 2. @SpringBootApplication annotation
// 3. application.properties or application.yml
// 4. pom.xml with spring-boot-starter-parent
.uses_framework(Framework::SpringBoot)
}
Custom Framework Detection
Define custom framework detection rules:
#![allow(unused)]
fn main() {
use refactor::discovery::FrameworkFilter;
let custom_framework = FrameworkFilter::custom("my-framework")
.requires_dependency("my-framework-core", "*")
.requires_file("my-framework.config.js")
.requires_pattern("src/**/*.myfw");
.repositories(|r| r
.framework(custom_framework))
}
Direct Usage
#![allow(unused)]
fn main() {
use refactor::discovery::FrameworkFilter;
let filter = FrameworkFilter::new(Framework::React);
// Check a single repository
if filter.matches(Path::new("./my-project"))? {
println!("Project uses React");
}
// Get detailed info
if let Some(info) = filter.detect(Path::new("./my-project"))? {
println!("Framework: {}", info.name);
println!("Version: {:?}", info.version);
println!("Confidence: {:.0}%", info.confidence * 100.0);
}
}
Confidence Scores
Detection returns confidence scores:
#![allow(unused)]
fn main() {
let result = FrameworkFilter::new(Framework::React)
.detect(Path::new("./project"))?;
match result {
Some(detection) if detection.confidence > 0.9 => {
println!("Definitely React");
}
Some(detection) if detection.confidence > 0.5 => {
println!("Probably React ({}%)", detection.confidence * 100.0);
}
Some(_) => {
println!("Might be React, low confidence");
}
None => {
println!("Not React");
}
}
}
Framework Version Detection
#![allow(unused)]
fn main() {
let detection = FrameworkFilter::new(Framework::NextJs)
.detect(Path::new("./project"))?;
if let Some(d) = detection {
if let Some(version) = d.version {
if version.major >= 13 {
println!("Uses App Router (Next.js 13+)");
} else {
println!("Uses Pages Router");
}
}
}
}
Use Cases
Framework Migration
#![allow(unused)]
fn main() {
// Find Create React App projects to migrate to Next.js
Codemod::from_github_org("company", token)
.repositories(|r| r
.uses_framework(Framework::React)
.has_dependency("react-scripts", "*")
.missing_framework(Framework::NextJs))
.collect_repos()?;
}
Framework Inventory
#![allow(unused)]
fn main() {
// Count framework usage across organization
let repos = Codemod::from_github_org("company", token)
.collect_all_repos()?;
let mut framework_counts = HashMap::new();
for repo in &repos {
for framework in Framework::all() {
if FrameworkFilter::new(framework).matches(&repo.path)? {
*framework_counts.entry(framework).or_insert(0) += 1;
}
}
}
}
Framework-Specific Transforms
#![allow(unused)]
fn main() {
Codemod::from_github_org("company", token)
.repositories(|r| r.uses_framework(Framework::Express))
.apply(|ctx| {
// Apply Express-specific security patches
ctx.add_middleware("helmet")
})
.execute()?;
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
let filter = FrameworkFilter::new(Framework::React);
match filter.matches(Path::new("./project")) {
Ok(true) => println!("Uses React"),
Ok(false) => println!("Doesn't use React"),
Err(RefactorError::IoError(e)) => {
println!("Failed to read project: {}", e);
}
Err(e) => return Err(e.into()),
}
}
See Also
- Dependency Filters - Lower-level package filtering
- Enhanced Discovery - Full discovery guide
Metrics Filters
Filter repositories based on code metrics like lines of code, file count, complexity, and more.
Overview
MetricFilter analyzes repositories to compute metrics and filter based on thresholds:
- Lines of code - Total source lines
- File count - Number of source files
- Average file size - Mean lines per file
- Cyclomatic complexity - Code complexity measure
- Test coverage - If coverage data is available
- Commit activity - Recent commit patterns
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find large projects
Codemod::from_github_org("company", token)
.repositories(|r| r
.lines_of_code(ComparisonOp::GreaterThan, 10000.0))
.apply(transform)
.execute()?;
}
Comparison Operators
#![allow(unused)]
fn main() {
pub enum ComparisonOp {
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual,
Equal,
NotEqual,
}
}
Available Metrics
Lines of Code
#![allow(unused)]
fn main() {
// Large projects
.lines_of_code(ComparisonOp::GreaterThan, 50000.0)
// Small projects
.lines_of_code(ComparisonOp::LessThan, 1000.0)
// Medium projects
.lines_of_code(ComparisonOp::GreaterThanOrEqual, 5000.0)
.lines_of_code(ComparisonOp::LessThan, 20000.0)
}
File Count
#![allow(unused)]
fn main() {
// Many files
.file_count(ComparisonOp::GreaterThan, 100.0)
// Few files (microservices)
.file_count(ComparisonOp::LessThan, 20.0)
}
Average File Size
#![allow(unused)]
fn main() {
// Large files (might need splitting)
.avg_file_size(ComparisonOp::GreaterThan, 500.0)
// Small, focused files
.avg_file_size(ComparisonOp::LessThan, 200.0)
}
Cyclomatic Complexity
#![allow(unused)]
fn main() {
// High complexity (might need refactoring)
.complexity(ComparisonOp::GreaterThan, 15.0)
// Low complexity
.complexity(ComparisonOp::LessThan, 10.0)
}
Test Coverage
#![allow(unused)]
fn main() {
// Well-tested projects
.coverage(ComparisonOp::GreaterThanOrEqual, 80.0)
// Under-tested projects
.coverage(ComparisonOp::LessThan, 50.0)
}
Commit Activity
#![allow(unused)]
fn main() {
// Active projects (commits in last N days)
.commits_in_days(30, ComparisonOp::GreaterThan, 5.0)
// Stale projects
.commits_in_days(90, ComparisonOp::LessThan, 1.0)
}
Language-Specific Metrics
Count only specific languages:
#![allow(unused)]
fn main() {
// TypeScript lines only
.lines_of_code_for("typescript", ComparisonOp::GreaterThan, 5000.0)
// Rust files only
.file_count_for("rust", ComparisonOp::GreaterThan, 10.0)
}
Multiple Metrics
Combine metrics (AND logic):
#![allow(unused)]
fn main() {
.repositories(|r| r
// Medium-sized, well-structured projects
.lines_of_code(ComparisonOp::GreaterThan, 5000.0)
.lines_of_code(ComparisonOp::LessThan, 50000.0)
.avg_file_size(ComparisonOp::LessThan, 300.0)
.complexity(ComparisonOp::LessThan, 12.0)
)
}
Direct Usage
#![allow(unused)]
fn main() {
use refactor::discovery::MetricFilter;
let filter = MetricFilter::lines_of_code(ComparisonOp::GreaterThan, 1000.0);
// Check a single repository
if filter.matches(Path::new("./my-project"))? {
println!("Project has more than 1000 lines");
}
// Get the actual metric value
let metrics = MetricFilter::compute_metrics(Path::new("./my-project"))?;
println!("Lines of code: {}", metrics.lines_of_code);
println!("File count: {}", metrics.file_count);
println!("Average file size: {:.1}", metrics.avg_file_size);
}
Metrics Report
Generate a full metrics report:
#![allow(unused)]
fn main() {
use refactor::discovery::MetricsReport;
let report = MetricsReport::for_repo(Path::new("./project"))?;
println!("Metrics Report for {}", report.path.display());
println!("================");
println!("Lines of code: {}", report.lines_of_code);
println!("Files: {}", report.file_count);
println!("Average file size: {:.1} lines", report.avg_file_size);
println!("Languages:");
for (lang, count) in &report.lines_by_language {
println!(" {}: {} lines ({:.1}%)",
lang, count, (*count as f64 / report.lines_of_code as f64) * 100.0);
}
// Export as JSON
let json = report.to_json()?;
}
Exclusions
Configure what to exclude from metrics:
#![allow(unused)]
fn main() {
let filter = MetricFilter::lines_of_code(ComparisonOp::GreaterThan, 1000.0)
.exclude_patterns(&[
"**/node_modules/**",
"**/target/**",
"**/vendor/**",
"**/*.min.js",
"**/generated/**",
])
.exclude_languages(&["json", "yaml", "xml"]);
}
Weighted Metrics
Create composite metrics:
#![allow(unused)]
fn main() {
use refactor::discovery::CompositeMetric;
// "Maintainability score"
let maintainability = CompositeMetric::new()
.add(MetricType::LinesOfCode, 0.3, |loc| {
// Score decreases with size
1.0 - (loc / 100000.0).min(1.0)
})
.add(MetricType::Complexity, 0.4, |c| {
// Score decreases with complexity
1.0 - (c / 25.0).min(1.0)
})
.add(MetricType::Coverage, 0.3, |cov| {
// Score increases with coverage
cov / 100.0
});
.repositories(|r| r
.composite_metric(maintainability, ComparisonOp::GreaterThan, 0.7))
}
Use Cases
Find Large Projects for Migration
#![allow(unused)]
fn main() {
// Large TypeScript projects that might benefit from strict mode
Codemod::from_github_org("company", token)
.repositories(|r| r
.primary_language("typescript")
.lines_of_code(ComparisonOp::GreaterThan, 10000.0))
.apply(enable_strict_mode)
.execute()?;
}
Find Stale Projects
#![allow(unused)]
fn main() {
// Projects with no recent activity
Codemod::from_github_org("company", token)
.repositories(|r| r
.commits_in_days(180, ComparisonOp::LessThan, 1.0))
.collect_repos()?;
}
Find Complex Code
#![allow(unused)]
fn main() {
// High-complexity projects needing refactoring
Codemod::from_github_org("company", token)
.repositories(|r| r
.complexity(ComparisonOp::GreaterThan, 20.0)
.lines_of_code(ComparisonOp::GreaterThan, 5000.0))
.collect_repos()?;
}
Prioritize by Size
#![allow(unused)]
fn main() {
// Start with small projects for gradual rollout
Codemod::from_github_org("company", token)
.repositories(|r| r
.uses_framework(Framework::React)
.lines_of_code(ComparisonOp::LessThan, 5000.0))
.apply(upgrade)
.execute()?;
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
let filter = MetricFilter::lines_of_code(ComparisonOp::GreaterThan, 1000.0);
match filter.matches(Path::new("./project")) {
Ok(true) => println!("Matches"),
Ok(false) => println!("Doesn't match"),
Err(RefactorError::IoError(e)) => {
println!("Failed to read files: {}", e);
}
Err(e) => return Err(e.into()),
}
}
Performance Considerations
Metrics computation can be slow for large repositories:
- Use caching - Results are cached by default
- Limit depth - Exclude
node_modules,target, etc. - Sample for estimates - For very large repos, sample a subset
- Run in parallel - Discovery parallelizes across repos
#![allow(unused)]
fn main() {
Codemod::from_github_org("company", token)
.repositories(|r| r
.lines_of_code(ComparisonOp::GreaterThan, 1000.0)
.cache_duration(Duration::from_secs(3600))
.parallel_discovery(true))
.collect_repos()?;
}
See Also
- Language Filters - Filter by programming language
- Enhanced Discovery - Full discovery guide
Language Filters
Filter repositories based on the programming languages they use.
Overview
LanguageFilter analyzes repositories to determine:
- Primary language - The dominant language by lines of code
- Language mix - Percentage breakdown of all languages
- Language presence - Whether a specific language is used at all
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Find Rust projects
Codemod::from_github_org("company", token)
.repositories(|r| r
.primary_language("rust"))
.apply(transform)
.execute()?;
}
Primary Language
Filter by the dominant language:
#![allow(unused)]
fn main() {
// Primarily Rust
.primary_language("rust")
// Primarily TypeScript
.primary_language("typescript")
// Primarily Python
.primary_language("python")
}
The primary language is determined by lines of code (excluding comments and blanks).
Language Percentage
Filter by language percentage:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// At least 80% TypeScript
.language_percentage("typescript", ComparisonOp::GreaterThanOrEqual, 80.0)
// Less than 20% JavaScript (mostly migrated)
.language_percentage("javascript", ComparisonOp::LessThan, 20.0)
// Significant Go presence
.language_percentage("go", ComparisonOp::GreaterThan, 30.0)
}
Language Presence
Check if any amount of a language is present:
#![allow(unused)]
fn main() {
// Contains any TypeScript
.has_language("typescript")
// Contains any Rust
.has_language("rust")
}
Multiple Languages
All Required (AND)
#![allow(unused)]
fn main() {
// TypeScript project with some Rust
.repositories(|r| r
.primary_language("typescript")
.has_language("rust"))
}
Exclude Languages
#![allow(unused)]
fn main() {
// TypeScript without JavaScript
.repositories(|r| r
.primary_language("typescript")
.excludes_language("javascript"))
}
Language Mix
#![allow(unused)]
fn main() {
// Mixed TypeScript/JavaScript projects
.repositories(|r| r
.language_percentage("typescript", ComparisonOp::GreaterThan, 40.0)
.language_percentage("javascript", ComparisonOp::GreaterThan, 30.0))
}
Supported Languages
Languages are detected by file extension:
| Language | Extensions |
|---|---|
| Rust | .rs |
| TypeScript | .ts, .tsx |
| JavaScript | .js, .jsx, .mjs |
| Python | .py, .pyi |
| Go | .go |
| Java | .java |
| C# | .cs |
| Ruby | .rb |
| C | .c, .h |
| C++ | .cpp, .cc, .hpp, .cxx |
| Swift | .swift |
| Kotlin | .kt, .kts |
| PHP | .php |
| Scala | .scala |
| Elixir | .ex, .exs |
| Haskell | .hs |
| Shell | .sh, .bash |
| HTML | .html, .htm |
| CSS | .css |
| SCSS | .scss, .sass |
| JSON | .json |
| YAML | .yaml, .yml |
| TOML | .toml |
| Markdown | .md |
Direct Usage
#![allow(unused)]
fn main() {
use refactor::discovery::LanguageFilter;
let filter = LanguageFilter::primary("rust");
// Check a single repository
if filter.matches(Path::new("./my-project"))? {
println!("Project is primarily Rust");
}
// Get language breakdown
let analysis = LanguageFilter::analyze(Path::new("./my-project"))?;
println!("Primary language: {}", analysis.primary);
println!("Language breakdown:");
for (lang, stats) in &analysis.languages {
println!(" {}: {} lines ({:.1}%)",
lang, stats.lines, stats.percentage);
}
}
Language Analysis
Get detailed language statistics:
#![allow(unused)]
fn main() {
use refactor::discovery::LanguageAnalysis;
let analysis = LanguageAnalysis::for_repo(Path::new("./project"))?;
println!("Repository: {}", analysis.repo_path.display());
println!("Primary: {} ({:.1}%)", analysis.primary, analysis.primary_percentage);
println!();
println!("All languages:");
for lang in analysis.ranked() {
println!(" {}: {} files, {} lines ({:.1}%)",
lang.name,
lang.files,
lang.lines,
lang.percentage);
}
}
Configuration
Exclude Patterns
#![allow(unused)]
fn main() {
let filter = LanguageFilter::primary("rust")
.exclude_patterns(&[
"**/target/**",
"**/vendor/**",
"**/node_modules/**",
]);
}
Minimum Threshold
Ignore languages below a threshold:
#![allow(unused)]
fn main() {
let filter = LanguageFilter::primary("rust")
.min_percentage(5.0); // Ignore languages < 5%
}
Count by Files vs Lines
#![allow(unused)]
fn main() {
// Default: count by lines
.primary_language("rust")
// Alternative: count by file count
.primary_language_by_files("rust")
}
Use Cases
Find Migration Candidates
#![allow(unused)]
fn main() {
// JavaScript projects to migrate to TypeScript
Codemod::from_github_org("company", token)
.repositories(|r| r
.primary_language("javascript")
.excludes_language("typescript"))
.collect_repos()?;
}
Find Polyglot Projects
#![allow(unused)]
fn main() {
// Projects using both frontend and backend languages
Codemod::from_github_org("company", token)
.repositories(|r| r
.has_language("typescript")
.has_language("go"))
.collect_repos()?;
}
Language Inventory
#![allow(unused)]
fn main() {
// Count language usage across org
let repos = Codemod::from_github_org("company", token)
.collect_all_repos()?;
let mut lang_counts: HashMap<String, usize> = HashMap::new();
let mut lang_lines: HashMap<String, usize> = HashMap::new();
for repo in &repos {
let analysis = LanguageAnalysis::for_repo(&repo.path)?;
for (lang, stats) in &analysis.languages {
*lang_counts.entry(lang.clone()).or_insert(0) += 1;
*lang_lines.entry(lang.clone()).or_insert(0) += stats.lines;
}
}
println!("Language usage across organization:");
for (lang, count) in lang_counts.iter().sorted_by_key(|(_, c)| Reverse(*c)) {
println!(" {}: {} repos, {} total lines",
lang, count, lang_lines.get(lang).unwrap_or(&0));
}
}
Find Pure Language Projects
#![allow(unused)]
fn main() {
// Pure Rust projects (no other languages)
Codemod::from_github_org("company", token)
.repositories(|r| r
.language_percentage("rust", ComparisonOp::GreaterThanOrEqual, 95.0))
.collect_repos()?;
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
let filter = LanguageFilter::primary("rust");
match filter.matches(Path::new("./project")) {
Ok(true) => println!("Primarily Rust"),
Ok(false) => println!("Not primarily Rust"),
Err(RefactorError::IoError(e)) => {
println!("Failed to analyze: {}", e);
}
Err(e) => return Err(e.into()),
}
}
Performance
Language analysis is fast but can be optimized:
- Exclude vendored code - Skip
node_modules,vendor, etc. - Cache results - Analysis is cached by default
- Sample large repos - For very large repos, sample directories
#![allow(unused)]
fn main() {
Codemod::from_github_org("company", token)
.repositories(|r| r
.primary_language("rust")
.cache_duration(Duration::from_secs(3600)))
.collect_repos()?;
}
See Also
- Metrics Filters - Filter by code metrics
- Enhanced Discovery - Full discovery guide
LSP Integration
Refactor DSL integrates with Language Server Protocol (LSP) servers to provide semantic refactoring capabilities that understand your code’s types, scopes, and references.
Why LSP?
While text-based and AST transforms are powerful, they lack semantic understanding:
| Feature | Text/AST | LSP |
|---|---|---|
| Find text patterns | Yes | Yes |
| Understand syntax | AST only | Yes |
| Understand types | No | Yes |
| Cross-file references | No | Yes |
| Rename with imports | No | Yes |
| Find all usages | Limited | Yes |
Quick Start
#![allow(unused)]
fn main() {
use refactor::lsp::LspRename;
// Rename a symbol semantically
let result = LspRename::new("src/main.rs", 10, 4, "new_function_name")
.auto_install() // Download LSP server if needed
.dry_run() // Preview changes
.execute()?;
println!("Would modify {} files:", result.file_count());
println!("{}", result.diff()?);
}
Supported Languages
Out of the box, Refactor DSL supports:
| Language | LSP Server | Extensions |
|---|---|---|
| Rust | rust-analyzer | .rs |
| TypeScript/JavaScript | typescript-language-server | .ts, .tsx, .js, .jsx |
| Python | pyright | .py, .pyi |
| Go | gopls | .go |
| C/C++ | clangd | .c, .h, .cpp, .hpp, .cc, .cxx |
Components
LspRegistry
Manages LSP server configurations for different languages:
#![allow(unused)]
fn main() {
let registry = LspRegistry::new(); // Includes defaults
let config = registry.find_for_file(Path::new("src/main.rs"));
}
LspInstaller
Downloads and installs LSP servers from the Mason registry:
#![allow(unused)]
fn main() {
let installer = LspInstaller::new()?;
let binary = installer.install("rust-analyzer")?;
}
LspRename
Performs semantic rename operations:
#![allow(unused)]
fn main() {
LspRename::find_symbol("src/lib.rs", "old_name", "new_name")?
.execute()?;
}
LspClient
Low-level client for LSP communication:
#![allow(unused)]
fn main() {
let mut client = LspClient::start(&config, &root_path)?;
client.initialize()?;
client.open_document(path)?;
let edit = client.rename(path, position, "new_name")?;
}
Architecture
┌─────────────────────────────────────────────────┐
│ LspRename │
│ (High-level semantic rename API) │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ LspRegistry │ │ LspInstaller│ │
│ │ │ │ │ │
│ │ Find server │ │ Download │ │
│ │ for file │ │ from Mason │ │
│ └─────────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────┤
│ LspClient │
│ (JSON-RPC communication with LSP server) │
├─────────────────────────────────────────────────┤
│ │
│ rust-analyzer │ tsserver │ pyright │ ... │
│ │
└─────────────────────────────────────────────────┘
Capabilities
Currently supported LSP operations:
- Rename - Rename symbols across files with import updates
- Find References - Locate all usages of a symbol
- Go to Definition - Find symbol definitions
Future planned operations:
- Extract function/method
- Inline variable
- Move to file
- Organize imports
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match LspRename::new("file.rs", 10, 4, "new_name").execute() {
Ok(result) => println!("Renamed in {} files", result.file_count()),
Err(RefactorError::UnsupportedLanguage(ext)) => {
println!("No LSP server for .{} files", ext);
}
Err(RefactorError::TransformFailed { message }) => {
println!("LSP error: {}", message);
}
Err(e) => return Err(e.into()),
}
}
See Also
- Configuration - Configure LSP servers
- Auto-Installation - Automatic server downloads
- Semantic Rename - Rename symbols safely
LSP Configuration
Configure LSP servers for different languages using LspRegistry and LspServerConfig.
LspRegistry
The registry manages server configurations for different languages:
#![allow(unused)]
fn main() {
use refactor::lsp::{LspRegistry, LspServerConfig};
// Create with defaults (rust-analyzer, tsserver, pyright, etc.)
let registry = LspRegistry::new();
// Create empty registry
let registry = LspRegistry::empty();
}
Finding Servers
#![allow(unused)]
fn main() {
let registry = LspRegistry::new();
// By file extension
let config = registry.find_by_extension("rs");
// By file path
let config = registry.find_for_file(Path::new("src/main.rs"));
// List all registered servers
for server in registry.all() {
println!("{}: {}", server.name, server.command);
}
}
Default Servers
LspRegistry::new() includes these defaults:
| Name | Command | Extensions | Root Markers |
|---|---|---|---|
| rust-analyzer | rust-analyzer | rs | Cargo.toml, rust-project.json |
| typescript-language-server | typescript-language-server --stdio | ts, tsx, js, jsx | tsconfig.json, jsconfig.json, package.json |
| pyright | pyright-langserver --stdio | py, pyi | pyproject.toml, setup.py, pyrightconfig.json |
| gopls | gopls serve | go | go.mod, go.work |
| clangd | clangd | c, h, cpp, hpp, cc, cxx | compile_commands.json, CMakeLists.txt, .clangd |
Custom Server Configuration
LspServerConfig Builder
#![allow(unused)]
fn main() {
use refactor::lsp::LspServerConfig;
let config = LspServerConfig::new("my-lsp", "/path/to/my-lsp")
.arg("--stdio")
.arg("--verbose")
.extensions(["myext", "myx"])
.root_markers(["myproject.json", ".myconfig"]);
}
Register Custom Servers
#![allow(unused)]
fn main() {
let mut registry = LspRegistry::new();
registry.register(
LspServerConfig::new("custom-lsp", "custom-language-server")
.arg("--stdio")
.extensions(["custom"])
.root_markers(["custom.config"])
);
// Now works with .custom files
let config = registry.find_by_extension("custom");
}
Root Detection
LSP servers need to know the project root. Root markers help find it:
#![allow(unused)]
fn main() {
let config = LspServerConfig::new("rust-analyzer", "rust-analyzer")
.root_markers(["Cargo.toml", "rust-project.json"]);
// Searches upward from file path to find root
let root = config.find_root(Path::new("src/main.rs"));
// Returns Some("/path/to/project") if Cargo.toml found
}
Using with LspClient
#![allow(unused)]
fn main() {
use refactor::lsp::{LspClient, LspRegistry};
let registry = LspRegistry::new();
let config = registry.find_for_file(Path::new("src/main.rs"))
.expect("No LSP for Rust files");
let root = config.find_root(Path::new("src/main.rs"))
.unwrap_or_else(|| PathBuf::from("."));
let mut client = LspClient::start(&config, &root)?;
client.initialize()?;
}
Using with LspRename
#![allow(unused)]
fn main() {
use refactor::lsp::{LspRename, LspServerConfig};
// Use default server (auto-detected)
let result = LspRename::new("src/main.rs", 10, 4, "new_name")
.execute()?;
// Use custom server
let custom = LspServerConfig::new("my-analyzer", "/opt/my-analyzer")
.arg("--stdio");
let result = LspRename::new("src/main.rs", 10, 4, "new_name")
.server(custom)
.execute()?;
}
Environment Requirements
LSP servers must be:
- Installed and in PATH - Or provide absolute path
- Executable - Proper permissions set
- Compatible - Support the LSP protocol
Check server availability:
# Rust
which rust-analyzer
# TypeScript
which typescript-language-server
# Python
which pyright-langserver
If not installed, use auto-installation.
Troubleshooting
Server Not Found
#![allow(unused)]
fn main() {
// Check if server command exists
use std::process::Command;
fn server_exists(command: &str) -> bool {
Command::new(command)
.arg("--version")
.output()
.is_ok()
}
}
Wrong Root Detection
#![allow(unused)]
fn main() {
// Explicitly set the root
LspRename::new("src/main.rs", 10, 4, "new_name")
.root("/path/to/project")
.execute()?;
}
Server Crashes
Enable debugging by checking server stderr (currently discarded):
#![allow(unused)]
fn main() {
// Modify LspClient::start to capture stderr for debugging
// (Feature enhancement planned)
}
LSP Auto-Installation
Refactor DSL can automatically download and install LSP servers from the Mason registry, the same registry used by Neovim’s mason.nvim.
Quick Start
#![allow(unused)]
fn main() {
use refactor::lsp::LspRename;
// Automatically install rust-analyzer if not in PATH
LspRename::find_symbol("src/main.rs", "old_fn", "new_fn")?
.auto_install()
.execute()?;
}
LspInstaller
For direct control over installations:
#![allow(unused)]
fn main() {
use refactor::lsp::LspInstaller;
let installer = LspInstaller::new()?;
// Install a server
let binary_path = installer.install("rust-analyzer")?;
println!("Installed to: {}", binary_path.display());
// Check if already installed
if installer.is_installed("rust-analyzer") {
println!("rust-analyzer is ready");
}
// Get path to installed binary
let path = installer.get_binary_path("rust-analyzer");
}
Available Servers
The installer supports any server in the Mason registry. Common ones:
| Package Name | Language |
|---|---|
rust-analyzer | Rust |
typescript-language-server | TypeScript/JavaScript |
pyright | Python |
gopls | Go |
clangd | C/C++ |
lua-language-server | Lua |
yaml-language-server | YAML |
json-lsp | JSON |
Browse all packages at mason-registry.dev.
Installation Directory
By default, servers are installed to:
- Linux/macOS:
~/.local/share/refactor/lsp-servers/ - Windows:
%LOCALAPPDATA%/refactor/lsp-servers/
Customize the location:
#![allow(unused)]
fn main() {
let installer = LspInstaller::new()?
.install_dir("/opt/lsp-servers")
.cache_dir("/tmp/lsp-cache");
}
Platform Detection
The installer automatically detects your platform:
#![allow(unused)]
fn main() {
use refactor::lsp::installer::{Platform, Os, Arch};
let platform = Platform::detect();
println!("OS: {:?}, Arch: {:?}", platform.os, platform.arch);
}
Supported platforms:
- OS: Linux, macOS, Windows
- Arch: x64, arm64, x86
Listing Installed Servers
#![allow(unused)]
fn main() {
let installer = LspInstaller::new()?;
for server in installer.list_installed()? {
println!("{} v{} at {}",
server.name,
server.version,
server.binary.display());
}
}
Uninstalling
#![allow(unused)]
fn main() {
installer.uninstall("rust-analyzer")?;
}
Ensure Installed
Install only if not already present:
#![allow(unused)]
fn main() {
// Returns path to binary, installing if needed
let binary = installer.ensure_installed("rust-analyzer")?;
}
How It Works
- Fetch metadata from Mason registry (
package.yaml) - Select asset for current platform
- Download binary/archive to cache
- Extract (supports
.gz,.tar.gz,.zip) - Install to installation directory
- Set permissions (executable on Unix)
Mason Registry (GitHub)
│
▼
┌───────────────┐
│ package.yaml │ ← Package metadata
└───────────────┘
│
▼
┌───────────────┐
│ GitHub Release│ ← Binary download
└───────────────┘
│
▼
┌───────────────┐
│ Local Cache │ ← Downloaded archive
└───────────────┘
│
▼
┌───────────────┐
│ Install Dir │ ← Extracted binary
└───────────────┘
Integration with LspRename
The auto_install() method on LspRename uses the installer:
#![allow(unused)]
fn main() {
LspRename::new("src/main.rs", 10, 4, "new_name")
.auto_install() // Enables auto-installation
.execute()?;
}
This will:
- Detect language from file extension
- Find appropriate LSP server
- Check if server exists in PATH
- If not, install from Mason registry
- Update config to use installed binary
- Proceed with rename operation
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match installer.install("unknown-server") {
Ok(path) => println!("Installed to {}", path.display()),
Err(RefactorError::TransformFailed { message }) => {
if message.contains("not found in registry") {
println!("Package doesn't exist in Mason registry");
} else if message.contains("No binary available") {
println!("No binary for this platform");
} else {
println!("Installation failed: {}", message);
}
}
Err(e) => return Err(e.into()),
}
}
Offline Usage
If you need to work offline:
- Pre-install servers while online
- Use explicit binary paths in
LspServerConfig - Disable
auto_install()
#![allow(unused)]
fn main() {
// Use pre-installed server
let config = LspServerConfig::new(
"rust-analyzer",
"/path/to/rust-analyzer"
);
LspRename::new("src/main.rs", 10, 4, "new_name")
.server(config) // Explicit server, no auto-install
.execute()?;
}
Semantic Rename
LspRename provides semantic symbol renaming that understands code structure, updates all references, and modifies imports across files.
Basic Usage
By Position
Rename the symbol at a specific location:
#![allow(unused)]
fn main() {
use refactor::lsp::LspRename;
// Rename symbol at line 10, column 4 (0-indexed)
let result = LspRename::new("src/main.rs", 10, 4, "new_name")
.execute()?;
println!("Modified {} files", result.file_count());
}
By Symbol Name
Find and rename a symbol by name:
#![allow(unused)]
fn main() {
// Find first occurrence of "old_function" and rename it
let result = LspRename::find_symbol("src/lib.rs", "old_function", "new_function")?
.execute()?;
}
Builder Methods
Project Root
Set the project root explicitly:
#![allow(unused)]
fn main() {
LspRename::new("src/main.rs", 10, 4, "new_name")
.root("/path/to/project")
.execute()?;
}
Custom LSP Server
Use a specific server configuration:
#![allow(unused)]
fn main() {
use refactor::lsp::LspServerConfig;
let config = LspServerConfig::new("my-analyzer", "/opt/my-analyzer")
.arg("--stdio");
LspRename::new("src/main.rs", 10, 4, "new_name")
.server(config)
.execute()?;
}
Auto-Installation
Automatically download the LSP server if needed:
#![allow(unused)]
fn main() {
LspRename::new("src/main.rs", 10, 4, "new_name")
.auto_install()
.execute()?;
}
Dry Run
Preview changes without applying:
#![allow(unused)]
fn main() {
let result = LspRename::new("src/main.rs", 10, 4, "new_name")
.dry_run()
.execute()?;
// Show what would change
println!("{}", result.diff()?);
}
RenameResult
The result provides information about the changes:
#![allow(unused)]
fn main() {
let result = LspRename::new("src/main.rs", 10, 4, "new_name")
.execute()?;
// Number of files affected
println!("Files: {}", result.file_count());
// Total number of edits
println!("Edits: {}", result.edit_count());
// Check if empty (symbol not found or not renameable)
if result.is_empty() {
println!("No changes made");
}
// Generate unified diff
println!("{}", result.diff()?);
// Was this a dry run?
if result.dry_run {
println!("Changes not applied (dry run)");
}
}
WorkspaceEdit
The underlying WorkspaceEdit contains all changes:
#![allow(unused)]
fn main() {
let edit = &result.workspace_edit;
// Iterate over file changes
for (path, edits) in edit.changes() {
println!("{}:", path.display());
for e in edits {
println!(" Line {}: {} -> {}",
e.range.start.line,
e.range,
e.new_text);
}
}
// Preview new content without applying
let previews = edit.preview()?;
for (path, new_content) in &previews {
println!("=== {} ===", path.display());
println!("{}", new_content);
}
// Apply changes (already done if not dry_run)
edit.apply()?;
}
Complete Example
#![allow(unused)]
fn main() {
use refactor::lsp::LspRename;
fn rename_api_function() -> Result<()> {
// Find the function to rename
let result = LspRename::find_symbol(
"src/api/handlers.rs",
"handle_request",
"process_request"
)?
.root("./my-project")
.auto_install()
.dry_run()
.execute()?;
if result.is_empty() {
println!("Symbol not found or not renameable");
return Ok(());
}
println!("Preview of changes:");
println!("{}", result.diff()?);
println!("\nWould modify {} files with {} edits",
result.file_count(),
result.edit_count());
// Ask for confirmation, then apply
println!("\nApply changes? [y/N]");
// ... get user input ...
// Apply without dry_run
LspRename::find_symbol(
"src/api/handlers.rs",
"handle_request",
"process_request"
)?
.root("./my-project")
.execute()?;
println!("Done!");
Ok(())
}
}
What Gets Renamed
A semantic rename updates:
- Function/method definitions
- Function/method calls
- Variable declarations and usages
- Type definitions and references
- Import/export statements
- Documentation references (server-dependent)
Example with Rust:
// Before: rename `process` to `handle`
// src/lib.rs
pub fn process(data: &str) -> String { ... }
// src/main.rs
use mylib::process;
fn main() {
let result = process("input");
}
// After
// src/lib.rs
pub fn handle(data: &str) -> String { ... }
// src/main.rs
use mylib::handle;
fn main() {
let result = handle("input");
}
Error Handling
#![allow(unused)]
fn main() {
use refactor::error::RefactorError;
match LspRename::find_symbol("src/main.rs", "not_found", "new_name") {
Ok(rename) => {
match rename.execute() {
Ok(result) if result.is_empty() => {
println!("Symbol found but not renameable");
}
Ok(result) => {
println!("Renamed in {} files", result.file_count());
}
Err(RefactorError::TransformFailed { message }) => {
println!("LSP error: {}", message);
}
Err(e) => return Err(e.into()),
}
}
Err(RefactorError::TransformFailed { message }) => {
println!("Symbol not found: {}", message);
}
Err(e) => return Err(e.into()),
}
}
Limitations
- Requires LSP server for the language
- Single symbol at a time (no batch rename)
- Server must support rename (most do)
- May miss dynamic references (reflection, eval, etc.)
See Also
Multi-Repository Refactoring
MultiRepoRefactor enables applying the same refactoring operation across multiple repositories at once.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
MultiRepoRefactor::new()
.repo("./project-a")
.repo("./project-b")
.repo("./project-c")
.matching(|m| m
.git(|g| g.branch("main").clean())
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_literal("old_api", "new_api"))
.apply()?;
}
Adding Repositories
Individual Repos
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.repo("./project-a")
.repo("./project-b")
}
Multiple at Once
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.repos(["./project-a", "./project-b", "./project-c"])
}
Discover in Directory
Find all Git repositories in a parent directory:
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")? // Finds all dirs with .git
}
Filtering Repositories
Use Git matchers to filter which repositories to process:
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g
.has_file("Cargo.toml") // Only Rust projects
.branch("main") // On main branch
.recent_commits(30) // Active in last 30 days
.clean())) // No uncommitted changes
.transform(/* ... */)
.apply()?;
}
Applying Transforms
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g.has_file("Cargo.toml"))
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"error\")"))
.dry_run() // Preview first!
.apply()?;
}
Handling Results
The result is a vector of per-repository results:
#![allow(unused)]
fn main() {
let results = MultiRepoRefactor::new()
.repos(["./project-a", "./project-b"])
.transform(|t| t.replace_literal("old", "new"))
.apply()?;
for (repo_path, result) in results {
match result {
Ok(ref_result) => {
println!("{}: modified {} files",
repo_path.display(),
ref_result.files_modified());
}
Err(e) => {
println!("{}: error - {}",
repo_path.display(),
e);
}
}
}
}
Dry Run Mode
Always preview changes across all repos first:
#![allow(unused)]
fn main() {
let results = MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m.files(|f| f.extension("rs")))
.transform(|t| t.replace_literal("old_name", "new_name"))
.dry_run()
.apply()?;
for (path, result) in &results {
if let Ok(r) = result {
if r.files_modified() > 0 {
println!("\n=== {} ===", path.display());
println!("{}", r.diff());
}
}
}
}
Complete Example
Update a deprecated API across all Rust projects in a workspace:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn update_api_across_workspace() -> Result<()> {
let results = MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
// Only Rust projects on main branch
.git(|g| g
.has_file("Cargo.toml")
.branch("main")
.clean())
// Only .rs files with the old API
.files(|f| f
.extension("rs")
.exclude("**/target/**")
.contains_pattern("deprecated_function")))
.transform(|t| t
.replace_pattern(
r"deprecated_function\((.*?)\)",
"new_function($1, Default::default())"
))
.dry_run()
.apply()?;
// Summary
let mut total_files = 0;
let mut repos_modified = 0;
for (path, result) in &results {
if let Ok(r) = result {
let files = r.files_modified();
if files > 0 {
repos_modified += 1;
total_files += files;
println!("{}: {} files", path.display(), files);
}
}
}
println!("\nTotal: {} files across {} repositories",
total_files, repos_modified);
Ok(())
}
}
Error Handling
Individual repository failures don’t stop the whole operation:
#![allow(unused)]
fn main() {
let results = multi_refactor.apply()?;
let (successes, failures): (Vec<_>, Vec<_>) = results
.into_iter()
.partition(|(_, r)| r.is_ok());
println!("Succeeded: {} repos", successes.len());
println!("Failed: {} repos", failures.len());
for (path, err) in failures {
println!(" {}: {}", path.display(), err.unwrap_err());
}
}
Use Cases
Dependency Updates
#![allow(unused)]
fn main() {
// Update version in all Cargo.toml files
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.files(|f| f.name_matches(r"^Cargo\.toml$")))
.transform(|t| t
.replace_pattern(
r#"my-lib = "1\.0""#,
r#"my-lib = "2.0""#
))
.apply()?;
}
Code Style Enforcement
#![allow(unused)]
fn main() {
// Add missing newlines at end of files
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g.has_file("Cargo.toml"))
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(r"([^\n])$", "$1\n"))
.apply()?;
}
License Header Updates
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.files(|f| f
.extension("rs")
.contains_pattern("// Copyright 2023")))
.transform(|t| t
.replace_literal("// Copyright 2023", "// Copyright 2024"))
.apply()?;
}
Limitations
- Transforms are applied independently to each repo
- No cross-repository dependency resolution
- Large workspaces may be slow (consider parallel processing)
Tips
- Always use dry_run first - Review changes before applying
- Use specific matchers - Avoid modifying unexpected repos
- Require clean state - Use
.git(|g| g.clean())to avoid conflicts - Check branch - Ensure you’re on the right branch
- Commit separately - Each repo should be committed individually
CLI Reference
The refactor CLI provides command-line access to refactoring operations.
Installation
# From source
git clone https://github.com/yourusername/refactor
cd refactor
cargo install --path .
# Verify
refactor --version
Commands
replace
Replace text patterns in files.
refactor replace [OPTIONS] [PATH]
Arguments:
PATH- Directory to process (default: current directory)
Options:
-p, --pattern <REGEX>- Pattern to search for (required)-r, --replacement <TEXT>- Replacement text (required)-e, --extension <EXT>- Filter by file extension-i, --include <GLOB>- Glob pattern to include--exclude <GLOB>- Glob pattern to exclude--dry-run- Preview changes without applying
Examples:
# Replace .unwrap() with .expect() in Rust files
refactor replace \
--pattern '\.unwrap\(\)' \
--replacement '.expect("error")' \
--extension rs \
--dry-run
# Replace in specific directory, excluding tests
refactor replace \
--pattern 'old_api' \
--replacement 'new_api' \
--extension rs \
--exclude '**/tests/**' \
./src
# Using capture groups
refactor replace \
--pattern 'fn (\w+)' \
--replacement 'pub fn $1' \
--extension rs
find
Find AST patterns in code using tree-sitter queries.
refactor find [OPTIONS] [PATH]
Arguments:
PATH- Directory to search (default: current directory)
Options:
-q, --query <QUERY>- Tree-sitter query pattern (required)-e, --extension <EXT>- Filter by file extension
Examples:
# Find all function definitions in Rust
refactor find \
--query '(function_item name: (identifier) @fn)' \
--extension rs
# Find struct definitions
refactor find \
--query '(struct_item name: (type_identifier) @struct)' \
--extension rs \
./src
Output format:
path/to/file.rs:10:5: function_name (fn)
path/to/file.rs:25:1: MyStruct (struct)
rename
Rename symbols across files (text-based, not semantic).
refactor rename [OPTIONS] [PATH]
Arguments:
PATH- Directory to process (default: current directory)
Options:
-f, --from <NAME>- Original symbol name (required)-t, --to <NAME>- New symbol name (required)-e, --extension <EXT>- Filter by file extension--dry-run- Preview changes without applying
Examples:
# Rename a function
refactor rename \
--from old_function \
--to new_function \
--extension rs \
--dry-run
# Rename across TypeScript files
refactor rename \
--from OldComponent \
--to NewComponent \
--extension tsx
Note: This is a text-based rename. For semantic rename that updates imports and references correctly, use the Rust API with
LspRename.
languages
List supported languages for AST operations.
refactor languages
Output:
Supported languages:
rust (extensions: rs)
typescript (extensions: ts, tsx, js, jsx)
python (extensions: py, pyi)
Global Options
--version- Print version information--help- Print help information
Output
Dry Run Output
When using --dry-run, the CLI shows a colorized diff:
--- src/main.rs
+++ src/main.rs
@@ -10,7 +10,7 @@
fn process_data() {
- let result = data.unwrap();
+ let result = data.expect("error");
process(result);
}
+1 -1 in 1 file(s)
Apply Output
Without --dry-run:
Modified 5 file(s)
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (invalid arguments, file errors, etc.) |
Examples
Common Workflows
Preview and apply:
# Preview
refactor replace -p 'old' -r 'new' -e rs --dry-run
# If satisfied, apply
refactor replace -p 'old' -r 'new' -e rs
Find before replace:
# Find occurrences first
refactor find -q '(call_expression function: (identifier) @fn)' -e rs
# Then replace
refactor replace -p 'deprecated_fn' -r 'new_fn' -e rs
Target specific directories:
# Only in src/
refactor replace -p 'TODO' -r 'DONE' -i '**/src/**' -e rs
# Exclude generated code
refactor replace -p 'old' -r 'new' --exclude '**/generated/**' -e rs
Shell Completion
Generate shell completions (planned feature):
# Bash
refactor completions bash > /etc/bash_completion.d/refactor
# Zsh
refactor completions zsh > ~/.zsh/completions/_refactor
# Fish
refactor completions fish > ~/.config/fish/completions/refactor.fish
See Also
API Reference
This page provides a quick reference to the main types and functions in Refactor DSL.
Prelude
Import commonly used types with:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
}
This includes:
| Type | Description |
|---|---|
Refactor | Main refactoring builder |
MultiRepoRefactor | Multi-repository refactoring |
RefactorResult | Result of a refactoring operation |
Matcher | Combined matcher builder |
FileMatcher | File filtering predicates |
GitMatcher | Git repository predicates |
AstMatcher | AST query matching |
Transform | Transform trait |
TransformBuilder | Composable transform builder |
TextTransform | Text-based transforms |
AstTransform | AST-aware transforms |
Language | Language trait |
LanguageRegistry | Language detection |
Rust, TypeScript, Python | Built-in languages |
LspClient | LSP protocol client |
LspRegistry | LSP server registry |
LspRename | Semantic rename |
LspInstaller | LSP server installer |
LspServerConfig | Server configuration |
RefactorError, Result | Error types |
Core Types
Refactor
#![allow(unused)]
fn main() {
impl Refactor {
// Create refactor for a repository
fn in_repo(path: impl Into<PathBuf>) -> Self;
fn current_dir() -> Result<Self>;
// Configure matching
fn matching<F>(self, f: F) -> Self
where F: FnOnce(Matcher) -> Matcher;
// Configure transforms
fn transform<F>(self, f: F) -> Self
where F: FnOnce(TransformBuilder) -> TransformBuilder;
// Options
fn dry_run(self) -> Self;
// Execute
fn apply(self) -> Result<RefactorResult>;
fn preview(self) -> Result<String>;
// Accessors
fn root(&self) -> &Path;
}
}
RefactorResult
#![allow(unused)]
fn main() {
impl RefactorResult {
fn files_modified(&self) -> usize;
fn diff(&self) -> String;
fn colorized_diff(&self) -> String;
// Fields
changes: Vec<FileChange>,
summary: DiffSummary,
}
}
Matcher
#![allow(unused)]
fn main() {
impl Matcher {
fn new() -> Self;
fn git<F>(self, f: F) -> Self
where F: FnOnce(GitMatcher) -> GitMatcher;
fn files<F>(self, f: F) -> Self
where F: FnOnce(FileMatcher) -> FileMatcher;
fn ast<F>(self, f: F) -> Self
where F: FnOnce(AstMatcher) -> AstMatcher;
fn matches_repo(&self, path: &Path) -> Result<bool>;
fn collect_files(&self, root: &Path) -> Result<Vec<PathBuf>>;
}
}
FileMatcher
#![allow(unused)]
fn main() {
impl FileMatcher {
fn new() -> Self;
fn extension(self, ext: impl Into<String>) -> Self;
fn extensions(self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self;
fn include(self, pattern: impl Into<String>) -> Self;
fn exclude(self, pattern: impl Into<String>) -> Self;
fn contains_pattern(self, pattern: impl Into<String>) -> Self;
fn name_matches(self, pattern: impl Into<String>) -> Self;
fn min_size(self, bytes: u64) -> Self;
fn max_size(self, bytes: u64) -> Self;
fn collect(&self, root: &Path) -> Result<Vec<PathBuf>>;
}
}
GitMatcher
#![allow(unused)]
fn main() {
impl GitMatcher {
fn new() -> Self;
fn branch(self, name: impl Into<String>) -> Self;
fn has_file(self, path: impl Into<String>) -> Self;
fn has_remote(self, name: impl Into<String>) -> Self;
fn recent_commits(self, days: u32) -> Self;
fn clean(self) -> Self;
fn dirty(self) -> Self;
fn has_uncommitted(self, has: bool) -> Self;
fn matches(&self, repo_path: &Path) -> Result<bool>;
}
}
AstMatcher
#![allow(unused)]
fn main() {
impl AstMatcher {
fn new() -> Self;
fn query(self, pattern: impl Into<String>) -> Self;
fn capture(self, name: impl Into<String>) -> Self;
fn find_matches(&self, source: &str, lang: &dyn Language) -> Result<Vec<AstMatch>>;
fn find_matches_in_file(&self, path: &Path, registry: &LanguageRegistry) -> Result<Vec<AstMatch>>;
fn has_matches(&self, source: &str, lang: &dyn Language) -> Result<bool>;
}
struct AstMatch {
text: String,
start_byte: usize,
end_byte: usize,
start_row: usize,
start_col: usize,
end_row: usize,
end_col: usize,
capture_name: String,
}
}
TransformBuilder
#![allow(unused)]
fn main() {
impl TransformBuilder {
fn new() -> Self;
fn replace_pattern(self, pattern: &str, replacement: &str) -> Self;
fn replace_literal(self, needle: &str, replacement: &str) -> Self;
fn ast<F>(self, f: F) -> Self
where F: FnOnce(AstTransform) -> AstTransform;
fn custom<T: Transform + 'static>(self, transform: T) -> Self;
fn apply(&self, source: &str, path: &Path) -> Result<String>;
fn describe(&self) -> Vec<String>;
}
}
TextTransform
#![allow(unused)]
fn main() {
impl TextTransform {
fn replace(pattern: &str, replacement: &str) -> Self;
fn replace_regex(pattern: Regex, replacement: impl Into<String>) -> Self;
fn replace_literal(needle: &str, replacement: &str) -> Self;
fn prepend_line(pattern: &str, prefix: &str) -> Result<Self>;
fn append_line(pattern: &str, suffix: &str) -> Result<Self>;
fn delete_lines(pattern: &str) -> Result<Self>;
fn insert_after(pattern: &str, content: &str) -> Result<Self>;
fn insert_before(pattern: &str, content: &str) -> Result<Self>;
}
}
LSP Types
LspRename
#![allow(unused)]
fn main() {
impl LspRename {
fn new(file_path: impl Into<PathBuf>, line: u32, column: u32, new_name: impl Into<String>) -> Self;
fn find_symbol(file_path: impl Into<PathBuf>, symbol_name: &str, new_name: impl Into<String>) -> Result<Self>;
fn root(self, path: impl Into<PathBuf>) -> Self;
fn server(self, config: LspServerConfig) -> Self;
fn dry_run(self) -> Self;
fn auto_install(self) -> Self;
fn execute(self) -> Result<RenameResult>;
}
struct RenameResult {
workspace_edit: WorkspaceEdit,
dry_run: bool,
}
impl RenameResult {
fn file_count(&self) -> usize;
fn edit_count(&self) -> usize;
fn is_empty(&self) -> bool;
fn diff(&self) -> Result<String>;
}
}
LspInstaller
#![allow(unused)]
fn main() {
impl LspInstaller {
fn new() -> Result<Self>;
fn install_dir(self, path: impl Into<PathBuf>) -> Self;
fn cache_dir(self, path: impl Into<PathBuf>) -> Self;
fn is_installed(&self, server_name: &str) -> bool;
fn get_binary_path(&self, server_name: &str) -> Option<PathBuf>;
fn ensure_installed(&self, server_name: &str) -> Result<PathBuf>;
fn install(&self, server_name: &str) -> Result<PathBuf>;
fn list_installed(&self) -> Result<Vec<InstalledServer>>;
fn uninstall(&self, server_name: &str) -> Result<()>;
}
}
LspServerConfig
#![allow(unused)]
fn main() {
impl LspServerConfig {
fn new(name: &str, command: &str) -> Self;
fn arg(self, arg: impl Into<String>) -> Self;
fn extensions(self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self;
fn root_markers(self, markers: impl IntoIterator<Item = impl Into<String>>) -> Self;
fn find_root(&self, file_path: &Path) -> Option<PathBuf>;
fn handles_extension(&self, ext: &str) -> bool;
}
}
Error Types
#![allow(unused)]
fn main() {
pub enum RefactorError {
Io(std::io::Error),
Git(git2::Error),
Glob(globset::Error),
Regex(regex::Error),
Query(tree_sitter::QueryError),
Json(serde_json::Error),
Parse { path: PathBuf, message: String },
RepoNotFound(PathBuf),
UnsupportedLanguage(String),
NoFilesMatched,
TransformFailed { message: String },
InvalidConfig(String),
}
pub type Result<T> = std::result::Result<T, RefactorError>;
}
See Also
- Getting Started
- Examples
- Generated rustdoc at docs.rs
Tree-sitter Queries
Tree-sitter queries use S-expression syntax to match patterns in parsed syntax trees. This reference covers the query language and common patterns.
Query Syntax
Basic Pattern
Match a node by type:
(identifier)
Named Children
Access specific child nodes:
(function_item name: (identifier))
Captures
Capture matched nodes with @name:
(function_item name: (identifier) @fn_name)
Anonymous Nodes
Match literal tokens with quotes:
(binary_expression operator: "+" @plus)
Wildcards
Match any node type:
(call_expression function: (_) @fn)
Alternations
Match multiple patterns:
[
(function_item name: (identifier) @fn)
(struct_item name: (type_identifier) @fn)
]
Quantifiers
?- Optional (0 or 1)*- Zero or more+- One or more
(function_item
(attribute_item)* @attrs
name: (identifier) @name)
Anchors
.- Anchor to start or end of siblings
(block . (expression_statement) @first) ; First statement
(block (expression_statement) @last .) ; Last statement
Predicates
#eq?
Match exact text:
((identifier) @fn
(#eq? @fn "main"))
#match?
Match regex pattern:
((identifier) @fn
(#match? @fn "^test_"))
#not-eq?, #not-match?
Negated versions:
((identifier) @fn
(#not-eq? @fn "main"))
Language Examples
Rust
#![allow(unused)]
fn main() {
// Function definitions
"(function_item name: (identifier) @fn)"
// Async functions
"(function_item (function_modifiers (async)) name: (identifier) @async_fn)"
// Public functions
"(function_item (visibility_modifier) name: (identifier) @pub_fn)"
// Struct definitions
"(struct_item name: (type_identifier) @struct)"
// Enum definitions
"(enum_item name: (type_identifier) @enum)"
// Impl blocks
"(impl_item type: (type_identifier) @impl_type)"
// Trait definitions
"(trait_item name: (type_identifier) @trait)"
// Use statements
"(use_declaration argument: (_) @import)"
// Macro invocations
"(macro_invocation macro: (identifier) @macro)"
// Method calls
"(call_expression
function: (field_expression field: (field_identifier) @method))"
// Unsafe blocks
"(unsafe_block) @unsafe"
// Attribute macros
"(attribute_item (attribute) @attr)"
// String literals
"(string_literal) @string"
// Function calls with specific name
"((call_expression
function: (identifier) @fn)
(#eq? @fn \"unwrap\"))"
}
TypeScript/JavaScript
#![allow(unused)]
fn main() {
// Function declarations
"(function_declaration name: (identifier) @fn)"
// Arrow functions
"(arrow_function) @arrow"
// Variable with arrow function
"(variable_declarator
name: (identifier) @name
value: (arrow_function))"
// Class declarations
"(class_declaration name: (type_identifier) @class)"
// Method definitions
"(method_definition name: (property_identifier) @method)"
// Interface declarations
"(interface_declaration name: (type_identifier) @interface)"
// Type aliases
"(type_alias_declaration name: (type_identifier) @type)"
// Import statements
"(import_statement) @import"
// Export statements
"(export_statement) @export"
// JSX elements
"(jsx_element
open_tag: (jsx_opening_element name: (_) @tag))"
// React hooks (functions starting with use)
"((call_expression
function: (identifier) @hook)
(#match? @hook \"^use\"))"
// Async functions
"(function_declaration (async) name: (identifier) @async_fn)"
}
Python
#![allow(unused)]
fn main() {
// Function definitions
"(function_definition name: (identifier) @fn)"
// Class definitions
"(class_definition name: (identifier) @class)"
// Method definitions (in class)
"(class_definition
body: (block
(function_definition name: (identifier) @method)))"
// Decorated functions
"(decorated_definition
definition: (function_definition name: (identifier) @fn))"
// Import statements
"(import_statement) @import"
"(import_from_statement) @from_import"
// Specific imports
"(import_from_statement
module_name: (dotted_name) @module)"
// Async functions
"(function_definition (async) name: (identifier) @async_fn)"
// Lambda expressions
"(lambda) @lambda"
// Docstrings
"(function_definition
body: (block . (expression_statement (string)) @docstring))"
// f-strings
"(string (interpolation) @fstring)"
// Type annotations
"(type) @type_annotation"
}
Go
#![allow(unused)]
fn main() {
// Function declarations
"(function_declaration name: (identifier) @fn)"
// Method declarations (with receiver)
"(method_declaration
receiver: (parameter_list) @receiver
name: (field_identifier) @method)"
// Struct types
"(type_declaration
(type_spec name: (type_identifier) @struct type: (struct_type)))"
// Interface types
"(type_declaration
(type_spec name: (type_identifier) @interface type: (interface_type)))"
// Package declaration
"(package_clause (package_identifier) @package)"
// Import declarations
"(import_declaration) @import"
"(import_spec path: (interpreted_string_literal) @path)"
// Function calls
"(call_expression function: (identifier) @fn)"
// Method calls
"(call_expression function: (selector_expression field: (field_identifier) @method))"
// Struct literals
"(composite_literal type: (type_identifier) @struct_type)"
// Variable declarations
"(var_declaration) @var"
"(short_var_declaration left: (expression_list (identifier) @var))"
// Constants
"(const_declaration) @const"
// Go routines
"(go_statement) @goroutine"
// Defer statements
"(defer_statement) @defer"
// Error handling pattern
"(if_statement
condition: (binary_expression
left: (identifier) @err
right: (nil))
(#eq? @err \"err\"))"
}
Java
#![allow(unused)]
fn main() {
// Class declarations
"(class_declaration name: (identifier) @class)"
// Interface declarations
"(interface_declaration name: (identifier) @interface)"
// Enum declarations
"(enum_declaration name: (identifier) @enum)"
// Method declarations
"(method_declaration name: (identifier) @method)"
// Constructor declarations
"(constructor_declaration name: (identifier) @constructor)"
// Field declarations
"(field_declaration declarator: (variable_declarator name: (identifier) @field))"
// Package declaration
"(package_declaration) @package"
// Import statements
"(import_declaration) @import"
// Annotations
"(annotation name: (identifier) @annotation)"
"(marker_annotation name: (identifier) @annotation)"
// Static methods
"(method_declaration
(modifiers (static))
name: (identifier) @static_method)"
// Abstract methods
"(method_declaration
(modifiers (abstract))
name: (identifier) @abstract_method)"
// Lambda expressions
"(lambda_expression) @lambda"
// Method invocations
"(method_invocation name: (identifier) @call)"
// Object creation
"(object_creation_expression type: (type_identifier) @type)"
// Try-catch blocks
"(try_statement) @try"
"(catch_clause) @catch"
// Spring annotations
"((annotation name: (identifier) @ann)
(#match? @ann \"^(Controller|Service|Repository|Component|Autowired)\"))"
}
C#
#![allow(unused)]
fn main() {
// Class declarations
"(class_declaration name: (identifier) @class)"
// Interface declarations
"(interface_declaration name: (identifier) @interface)"
// Struct declarations
"(struct_declaration name: (identifier) @struct)"
// Record declarations (C# 9+)
"(record_declaration name: (identifier) @record)"
// Enum declarations
"(enum_declaration name: (identifier) @enum)"
// Method declarations
"(method_declaration name: (identifier) @method)"
// Property declarations
"(property_declaration name: (identifier) @property)"
// Field declarations
"(field_declaration (variable_declaration
(variable_declarator (identifier) @field)))"
// Constructor declarations
"(constructor_declaration name: (identifier) @constructor)"
// Namespace declarations
"(namespace_declaration name: (_) @namespace)"
// Using directives
"(using_directive) @using"
// Attributes
"(attribute name: (identifier) @attribute)"
// Async methods
"(method_declaration
(modifier (async))
name: (identifier) @async_method)"
// Static methods
"(method_declaration
(modifier (static))
name: (identifier) @static_method)"
// Lambda expressions
"(lambda_expression) @lambda"
// LINQ queries
"(query_expression) @linq"
// Method invocations
"(invocation_expression
function: (member_access_expression name: (identifier) @call))"
// Object creation
"(object_creation_expression type: (identifier) @type)"
// Pattern matching
"(switch_expression) @switch_expression"
"(is_pattern_expression) @is_pattern"
// Null-conditional access
"(conditional_access_expression) @null_conditional"
// ASP.NET attributes
"((attribute name: (identifier) @attr)
(#match? @attr \"^(HttpGet|HttpPost|Route|Authorize|ApiController)\"))"
}
Ruby
#![allow(unused)]
fn main() {
// Class definitions
"(class name: (constant) @class)"
// Module definitions
"(module name: (constant) @module)"
// Method definitions
"(method name: (identifier) @method)"
// Singleton method definitions (class methods)
"(singleton_method name: (identifier) @class_method)"
// Blocks
"(block) @block"
"(do_block) @do_block"
// Lambda expressions
"(lambda) @lambda"
// Require statements
"(call method: (identifier) @req (#eq? @req \"require\"))"
"(call method: (identifier) @req (#eq? @req \"require_relative\"))"
// Include/extend
"(call method: (identifier) @inc (#eq? @inc \"include\"))"
"(call method: (identifier) @ext (#eq? @ext \"extend\"))"
// Attr accessors
"(call method: (identifier) @attr (#match? @attr \"^attr_\"))"
// Instance variables
"(instance_variable) @ivar"
// Class variables
"(class_variable) @cvar"
// Constants
"(constant) @const"
// Symbols
"(simple_symbol) @symbol"
"(hash_key_symbol) @symbol"
// Method calls
"(call method: (identifier) @call)"
// Method calls with receiver
"(call
receiver: (_)
method: (identifier) @method_call)"
// Rescue blocks
"(rescue) @rescue"
"(ensure) @ensure"
// Yield
"(yield) @yield"
// Rails-specific patterns
// Model callbacks
"((call method: (identifier) @cb)
(#match? @cb \"^(before_|after_|around_)\"))"
// Associations
"((call method: (identifier) @assoc)
(#match? @assoc \"^(has_many|has_one|belongs_to|has_and_belongs_to_many)\"))"
// Validations
"((call method: (identifier) @val)
(#match? @val \"^validates\"))"
// Controller actions
"(method name: (identifier) @action
(#match? @action \"^(index|show|new|create|edit|update|destroy)\"))"
}
Query Debugging
Using tree-sitter CLI
# Install tree-sitter CLI
npm install -g tree-sitter-cli
# Parse a file and see the tree
tree-sitter parse file.rs
# Run a query against a file
tree-sitter query query.scm file.rs
Online Playground
Use the tree-sitter playground to interactively develop queries.
In Refactor DSL
#![allow(unused)]
fn main() {
use refactor::prelude::*;
// Test query validity
let result = Rust.query("(function_item @fn)");
match result {
Ok(_) => println!("Valid query"),
Err(e) => println!("Invalid: {:?}", e),
}
// See what matches
let matches = AstMatcher::new()
.query("(function_item name: (identifier) @fn)")
.find_matches(source, &Rust)?;
for m in matches {
println!("{:?}", m);
}
}
Common Patterns
Find All Definitions
#![allow(unused)]
fn main() {
// Functions, structs, enums in Rust
"[
(function_item name: (identifier) @def)
(struct_item name: (type_identifier) @def)
(enum_item name: (type_identifier) @def)
]"
}
Find Specific Function Calls
#![allow(unused)]
fn main() {
// Calls to deprecated_api
"((call_expression
function: (identifier) @fn)
(#eq? @fn \"deprecated_api\"))"
// Method calls to .unwrap()
"((call_expression
function: (field_expression field: (field_identifier) @method))
(#eq? @method \"unwrap\"))"
}
Find TODO Comments
#![allow(unused)]
fn main() {
// Line comments containing TODO
"((line_comment) @comment
(#match? @comment \"TODO\"))"
}
Find Test Functions
#![allow(unused)]
fn main() {
// Rust test functions
"(attribute_item (attribute) @attr
(#eq? @attr \"test\"))
(function_item name: (identifier) @test_fn)"
// Python test functions
"((function_definition name: (identifier) @fn)
(#match? @fn \"^test_\"))"
}
Performance Tips
- Be specific - More specific patterns are faster
- Use predicates sparingly -
#eq?is faster than#match? - Capture only what you need - Extra captures add overhead
- Test incrementally - Start simple, add complexity