Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. As a Rust library - Integrate into your Rust projects for programmatic refactoring
  2. 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

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, RefactorResult
  • Matcher, FileMatcher, GitMatcher, AstMatcher
  • Transform, TransformBuilder, TextTransform, AstTransform
  • Language, LanguageRegistry, Rust, TypeScript, Python, Go, Java, CSharp, Ruby
  • LspClient, 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:

DependencyPurpose
tree-sitterMulti-language parsing
git2Git repository operations
walkdirFile system traversal
globsetGlob pattern matching
regexRegular expressions
lsp-typesLSP protocol types

Optional LSP Servers

For semantic refactoring (rename, find references), you’ll need language servers:

LanguageServerInstall
Rustrust-analyzerrustup component add rust-analyzer
TypeScripttypescript-language-servernpm i -g typescript-language-server
Pythonpyrightnpm i -g pyright
Gogoplsgo install golang.org/x/tools/gopls@latest
JavajdtlsEclipse JDT Language Server
C#omnisharpdotnet tool install -g OmniSharp
Rubysolargraphgem install solargraph
C/C++clangdSystem 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:

  1. Extension check (fastest)
  2. Glob include/exclude
  3. Name regex
  4. Size check
  5. 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"),
    &registry,
)?;
}

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:

  1. Use the tree-sitter CLI: tree-sitter parse file.rs
  2. Use the tree-sitter playground
  3. 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

  1. Use literal replacement when possible - Faster than regex
  2. Be specific with patterns - ^\s*fn is faster than fn
  3. Order transforms efficiently - Put common replacements first
  4. 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:

  1. Read-only analysis - AST queries find patterns but don’t directly modify the tree
  2. No semantic understanding - Doesn’t understand types, scopes, or references
  3. 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

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

LanguageExtensionsTree-sitter GrammarLSP Server
Rust.rstree-sitter-rustrust-analyzer
TypeScript.ts, .tsx, .js, .jsxtree-sitter-typescripttypescript-language-server
Python.py, .pyitree-sitter-pythonpyright
Go.gotree-sitter-gogopls
Java.javatree-sitter-javajdtls
C#.cstree-sitter-c-sharpomnisharp
Ruby.rb, .rake, .gemspectree-sitter-rubysolargraph
C/C++.c, .h, .cpp, .hpptree-sitter-cppclangd

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:

  1. Add the tree-sitter grammar dependency to Cargo.toml
  2. Implement the Language trait
  3. 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

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:

  1. Parameters - Variables used but not defined in the selection
  2. Return value - Values computed and used after the selection
  3. 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:

LanguageFunctionVariableConstant
RustYesYesYes
TypeScriptYesYesYes
PythonYesYesYes
GoYesYesYes
JavaYesYesYes
C#YesYesYes
RubyYesYesLimited

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

  1. Use meaningful names - Choose names that describe what the extracted code does
  2. Start small - Extract simple, self-contained code first
  3. Preview changes - Always use .dry_run() before applying
  4. Check parameters - Review inferred parameters for correctness
  5. Consider visibility - Use the minimum visibility needed

See Also

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

LanguageVariableFunction
RustYesYes
TypeScriptYesYes
PythonYesYes
GoYesYes
JavaYesYes
C#YesYes
RubyYesYes

Best Practices

  1. Start with single usage - Inline one call site to verify correctness
  2. Check for side effects - Ensure inlining doesn’t change behavior
  3. Consider readability - Sometimes the named entity is clearer
  4. Preview changes - Always use .dry_run() first
  5. Clean up after - Remove unused definitions

See Also

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");
}

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

LanguageMoveToFileMoveBetweenModules
RustYesYes
TypeScriptYesYes
PythonYesYes
GoYesYes
JavaYesYes
C#YesYes
RubyYesLimited

Best Practices

  1. Preview changes - Always use .dry_run() first
  2. Add re-exports initially - Remove them after migration period
  3. Move related items together - Use .include_related(true)
  4. Update tests - Move operations update source files, check test imports
  5. Commit before moving - Have a clean git state to easily revert

See Also

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
  • *args and **kwargs are preserved
  • Decorators are maintained

Go

  • Named return values are supported
  • Multiple return values are handled
  • Interface implementations need matching changes

Best Practices

  1. Preview changes - Always use .dry_run() first
  2. Start with additions - Add parameters before removing old ones
  3. Provide meaningful defaults - Help callers migrate gradually
  4. Update tests - Test call sites may need attention
  5. 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

LanguageFunctionTypeMethodVariableImport
RustYesYesYesYesYes
TypeScriptYesYesYesYesYes
PythonYesYesYesYesYes
GoYesYesYesYesYes
JavaYesYesYesYesYes
C#YesYesYesYesYes
RubyYesYesYesYesYes

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

  1. Always preview first - Use .check_usages(true) before deletion
  2. Prefer cascade sparingly - Review what will be deleted
  3. Check tests - Tests may have usages not in main source
  4. Consider deprecation first - Mark as deprecated before deleting
  5. Keep git clean - Easy to revert if something breaks

See Also

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-ignore comments

Python

  • Handles __all__ exports
  • Considers if __name__ == "__main__" blocks
  • Respects # noqa comments

Go

  • Considers exported (capitalized) identifiers
  • Handles init() functions
  • Respects //nolint comments

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

  1. Reflection/runtime usage - Code used via strings/macros
  2. FFI exports - Functions called from C/other languages
  3. Framework callbacks - Methods called by framework magic
  4. 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

LanguageFunctionsTypesImportsVariablesParameters
RustYesYesYesYesYes
TypeScriptYesYesYesYesYes
PythonYesYesYesYesYes
GoYesYesYesYesYes
JavaYesYesYesYesYes
C#YesYesYesYesYes
RubyYesYesYesYesLimited

Best Practices

  1. Run regularly - Include in CI/CD pipeline
  2. Start conservative - Use consider_exports_used(true) for libraries
  3. Review before deleting - Some “dead” code may be needed
  4. Exclude tests initially - Test utilities may seem unused
  5. Handle false positives - Use annotations or skip patterns

See Also

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

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

  1. Specify ecosystem - Use npm(), cargo(), etc. instead of generic has_dependency()
  2. Avoid transitive - Only enable when necessary
  3. Cache results - Discovery caches manifest parsing

See Also

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

FrameworkDetection Method
Reactreact dependency + JSX files
Next.jsnext.config.* + next dependency
Vuevue dependency + .vue files
Nuxtnuxt.config.* + nuxt dependency
Angularangular.json + @angular/core
Sveltesvelte.config.* + svelte dependency
Expressexpress + server patterns
NestJS@nestjs/core + decorators
Gatsbygatsby-config.* + gatsby
Remixremix.config.* + @remix-run/*

Python

FrameworkDetection Method
Djangodjango + manage.py + settings.py
Flaskflask + app.py patterns
FastAPIfastapi + async patterns
Pyramidpyramid dependency
Tornadotornado dependency

Ruby

FrameworkDetection Method
Railsrails gem + config/application.rb
Sinatrasinatra gem + app patterns
Hanamihanami gem

Java

FrameworkDetection Method
Spring Bootspring-boot-* + @SpringBootApplication
Springspring-* dependencies
Quarkusquarkus-* + application.properties
Micronautmicronaut-*
Jakarta EEjakarta.* dependencies

Go

FrameworkDetection Method
Gingithub.com/gin-gonic/gin
Echogithub.com/labstack/echo
Fibergithub.com/gofiber/fiber
Chigithub.com/go-chi/chi

Rust

FrameworkDetection Method
Actixactix-web crate
Axumaxum crate
Rocketrocket crate
Warpwarp crate

C#

FrameworkDetection Method
ASP.NET CoreMicrosoft.AspNetCore.*
BlazorMicrosoft.AspNetCore.Components.*
WPFPresentationFramework reference
WinFormsSystem.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

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:

  1. Use caching - Results are cached by default
  2. Limit depth - Exclude node_modules, target, etc.
  3. Sample for estimates - For very large repos, sample a subset
  4. 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 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:

LanguageExtensions
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:

  1. Exclude vendored code - Skip node_modules, vendor, etc.
  2. Cache results - Analysis is cached by default
  3. 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

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:

FeatureText/ASTLSP
Find text patternsYesYes
Understand syntaxAST onlyYes
Understand typesNoYes
Cross-file referencesNoYes
Rename with importsNoYes
Find all usagesLimitedYes

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:

LanguageLSP ServerExtensions
Rustrust-analyzer.rs
TypeScript/JavaScripttypescript-language-server.ts, .tsx, .js, .jsx
Pythonpyright.py, .pyi
Gogopls.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

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:

NameCommandExtensionsRoot Markers
rust-analyzerrust-analyzerrsCargo.toml, rust-project.json
typescript-language-servertypescript-language-server --stdiots, tsx, js, jsxtsconfig.json, jsconfig.json, package.json
pyrightpyright-langserver --stdiopy, pyipyproject.toml, setup.py, pyrightconfig.json
goplsgopls servegogo.mod, go.work
clangdclangdc, h, cpp, hpp, cc, cxxcompile_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:

  1. Installed and in PATH - Or provide absolute path
  2. Executable - Proper permissions set
  3. 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 NameLanguage
rust-analyzerRust
typescript-language-serverTypeScript/JavaScript
pyrightPython
goplsGo
clangdC/C++
lua-language-serverLua
yaml-language-serverYAML
json-lspJSON

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

  1. Fetch metadata from Mason registry (package.yaml)
  2. Select asset for current platform
  3. Download binary/archive to cache
  4. Extract (supports .gz, .tar.gz, .zip)
  5. Install to installation directory
  6. 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:

  1. Detect language from file extension
  2. Find appropriate LSP server
  3. Check if server exists in PATH
  4. If not, install from Mason registry
  5. Update config to use installed binary
  6. 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:

  1. Pre-install servers while online
  2. Use explicit binary paths in LspServerConfig
  3. 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

  1. Always use dry_run first - Review changes before applying
  2. Use specific matchers - Avoid modifying unexpected repos
  3. Require clean state - Use .git(|g| g.clean()) to avoid conflicts
  4. Check branch - Ensure you’re on the right branch
  5. 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

CodeMeaning
0Success
1Error (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:

TypeDescription
RefactorMain refactoring builder
MultiRepoRefactorMulti-repository refactoring
RefactorResultResult of a refactoring operation
MatcherCombined matcher builder
FileMatcherFile filtering predicates
GitMatcherGit repository predicates
AstMatcherAST query matching
TransformTransform trait
TransformBuilderComposable transform builder
TextTransformText-based transforms
AstTransformAST-aware transforms
LanguageLanguage trait
LanguageRegistryLanguage detection
Rust, TypeScript, PythonBuilt-in languages
LspClientLSP protocol client
LspRegistryLSP server registry
LspRenameSemantic rename
LspInstallerLSP server installer
LspServerConfigServer configuration
RefactorError, ResultError 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

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

  1. Be specific - More specific patterns are faster
  2. Use predicates sparingly - #eq? is faster than #match?
  3. Capture only what you need - Extra captures add overhead
  4. Test incrementally - Start simple, add complexity

Resources