Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,49 @@ just quality-tools # Auto-format code with rustfmt

### Running the Tool

**Single file mode** — refactor one PHP file:
```bash
cargo run -- path/to/file.php
# or
./target/debug/php-refactor path/to/file.php
cargo run -- app.php
./target/debug/php-refactor app.php
```

**Directory mode** — refactor all PHP files in a directory (recursive):
```bash
cargo run -- src/
./target/debug/php-refactor tests/
```

**Config mode** — refactor multiple directories defined in a TOML config file:
```bash
cargo run -- config.toml
./target/debug/php-refactor target/config.toml
```

### Config File Format

Create a `config.toml` file to process multiple directories at once:

```toml
[source]
paths = ["src", "tests", "app"]
```

Paths can be:
- Relative to the project root (where you run the command): `"src"`, `"./app"`
- Absolute: `"/home/user/project/src"`
- Nested: `"src/components/php"`

The tool will recursively walk each path and apply all rules to every `.php` file found.

## How Rules Work

Rules receive a **path** (file, directory, or config file) and are responsible for:
1. Expanding it to actual PHP files (single file, directory walk, config parsing)
2. Reading each file, applying transformations, writing back if changed
3. Returning stats: how many files changed and how many were analyzed

This design keeps `main.rs` simple — it just passes the input path to each rule in sequence. Each rule is autonomous and can process different subsets of files if needed.

## How to Find What You Need

- **Writing or modifying a rule?** → See `.claude/rules/rule-contract.md` (function contract, return semantics)
Expand All @@ -34,21 +71,25 @@ cargo run -- path/to/file.php
- **Looking at architecture in `src/`?** → `.claude/rules/architecture.md` (loaded automatically when working in src/)
- **Understanding test fixtures in `tests/`?** → `.claude/rules/testing-patterns.md` (loaded automatically when working in tests/)
- **Need a definition?** → Invoke `/glossary` skill (Rule, Fixture, AST, Span, Bump Arena, Idempotent, mago-syntax)
- **Understanding file discovery?** → See `src/resolver.rs` (handles single files, directories, and TOML config expansion)

## Project Structure

```
src/
├── main.rs # CLI entry, rule chaining, file I/O
├── main.rs # CLI entry: get path, loop rules, report results
├── lib.rs # Module re-exports
├── reporter.rs # Timing and memory reporting
└── rules/mod.rs # Rule registry (all_rules)
└── rules/
├── mod.rs # Rule registry (all_rules, all_source_transforms)
└── quality/ # Quality rules by category
├── mod.rs
└── add_final_keyword.rs # Example rule (adds final to classes)

tests/
├── rules_test.rs # Fixture-based integration test runner
└── rules/ # Test fixtures by rule path
├── *_test.rs # Individual unit tests
├── rules_test.rs # Rule integration tests (uses source fixtures)
└── fixtures/ # Versioned fixtures for test data

.github/workflows/quality.yml # CI: check, fmt, clippy, test
Cargo.toml / Cargo.lock # Dependencies
justfile # Task automation
Cargo.toml / Cargo.lock # Dependencies
justfile # Task automation
```
62 changes: 62 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ mago-syntax = "1.15.3"
mago-span = "1.15.3"
mago-database = "1.15.3"
bumpalo = "3"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
walkdir = "2"
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ docker run --rm -v $(pwd):/workspace php-refactor \

## Usage

The tool accepts a **single argument**: a file path, directory path, or config file path.

### Single File Mode

Process one PHP file:

```bash
php-refactor path/to/file.php
php-refactor MyClass.php
```

**Example:**
Expand All @@ -54,7 +60,40 @@ final class MyClass {
}
```

The tool modifies the file in-place. If no rules apply, the file is left unchanged.
### Directory Mode

Recursively process all PHP files in a directory:

```bash
php-refactor src/
php-refactor tests/
```

### Config File Mode

Process multiple directories defined in a TOML config file:

```bash
php-refactor config.toml
```

**config.toml:**

```toml
[source]
paths = ["src", "tests", "app"]
```

Paths can be:
- Relative to the project root: `"src"`, `"./app"`
- Absolute: `"/home/user/project/src"`
- Nested: `"src/components/php"`

The tool will recursively walk each path and apply all rules to every `.php` file found.

---

The tool modifies files in-place. If no rules apply, files are left unchanged.

### Output

Expand Down
40 changes: 40 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# PHP Refactor Rules

Detailed documentation of all available transformation rules.

---

## `quality/add_final_keyword`

**Summary**: Adds `final` keyword to concrete classes to prevent accidental subclassing.

**When to use**: Enforce class design intent — mark classes that shouldn't be extended as `final`.

**What it does:**
- Adds `final` keyword before any non-abstract, non-final class declaration
- Supports classes inside namespaces
- Handles `readonly` classes: produces `final readonly class`
- Returns `None` (no change) if already processed

**What it skips:**
- Abstract classes (marked `abstract`)
- Classes that already have `final`
- Interfaces and traits (no `final` modifier in PHP)
- Enums (PHP enums cannot be marked `final`)

**Example:**

```php
// Before
class Service {}
readonly class Config {}
namespace App;
class Model {}

// After
final class Service {}
final readonly class Config {}
namespace App;
final class Model {}
```

5 changes: 3 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ docker-build:
build:
docker compose run --rm app cargo build

# Build release binary (optimized)
# Build release binary (optimized, auto-detects OS)
build-release:
docker compose run --rm app cargo build --release
cargo build --release
bash scripts/rename-binary.sh

# Auto-fix code: format + run tests
quality-tools:
Expand Down
4 changes: 4 additions & 0 deletions scripts/rename-binary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
VERSION=$(grep '^version' Cargo.toml | grep -o '"[^"]*"' | head -1 | tr -d '"')
mv target/release/php-refactor target/release/php-refactor-${VERSION}
echo "Binary available at: $(pwd)/target/release/php-refactor-${VERSION}"
18 changes: 18 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use serde::Deserialize;
use std::fs;

#[derive(Deserialize)]
pub struct Config {
pub source: SourceConfig,
}

#[derive(Deserialize)]
pub struct SourceConfig {
pub paths: Vec<String>,
}

pub fn load(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let contents = fs::read_to_string(path)?;
let config = toml::from_str(&contents)?;
Ok(config)
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod config;
pub mod reporter;
pub mod resolver;
pub mod rules;
Loading