rustor-rule-creator
Create PHP refactoring rules for the rustor Rust project. Use when implementing new rules, converting PHP patterns to array_find/array_any/array_all, modernizing PHP syntax, or adding refactoring transformations. Automatically generates Rust code, tests, registers rules, and validates with cargo test.
SKILL.md
| Name | rustor-rule-creator |
| Description | Create PHP refactoring rules for the rustor Rust project. Use when implementing new rules, converting PHP patterns to array_find/array_any/array_all, modernizing PHP syntax, or adding refactoring transformations. Automatically generates Rust code, tests, registers rules, and validates with cargo test. |
name: rustor-rule-creator description: Create PHP refactoring rules for the rustor Rust project. Use when implementing new rules, converting PHP patterns to array_find/array_any/array_all, modernizing PHP syntax, or adding refactoring transformations. Automatically generates Rust code, tests, registers rules, and validates with cargo test. allowed-tools:
- Read
- Grep
- Glob
- Bash
- Write
- Edit
Rustor Rule Creator
Efficiently implement PHP refactoring rules in the rustor project.
Overview
Rustor is a PHP refactoring tool written in Rust. This skill helps you:
- Design rule patterns from PHP code examples
- Generate the Rust rule implementation
- Register the rule in the system
- Create comprehensive tests
- Validate everything compiles and passes
Workflow
Step 1: Understand the Pattern
When given a PHP transformation, identify:
- Before pattern: The PHP code to match
- After pattern: The replacement PHP code
- AST nodes involved: FuncCall, Expression, Statement, etc.
- Variables to capture: Expressions that vary between instances
Step 2: Generate Rule File
Create crates/rustor-rules/src/{rule_name}.rs with this structure:
//! Rule: {description}
//!
//! Pattern:
//! ```php
//! // Before
//! {before_php}
//!
//! // After
//! {after_php}
//! ```
use mago_span::{HasSpan, Span};
use mago_syntax::ast::*;
use rustor_core::Edit;
use crate::registry::{Category, PhpVersion, Rule};
pub fn check_{rule_name}<'a>(program: &Program<'a>, source: &str) -> Vec<Edit> {
let mut checker = {RuleName}Checker {
source,
edits: Vec::new(),
};
checker.check_program(program);
checker.edits
}
struct {RuleName}Checker<'s> {
source: &'s str,
edits: Vec<Edit>,
}
impl<'s> {RuleName}Checker<'s> {
fn get_text(&self, span: Span) -> &str {
&self.source[span.start.offset as usize..span.end.offset as usize]
}
fn check_program(&mut self, program: &Program<'_>) {
self.check_statement_sequence(program.statements.as_slice());
}
fn check_statement(&mut self, stmt: &Statement<'_>) {
// Recurse into nested structures
match stmt {
Statement::Function(func) => self.check_block(&func.body),
Statement::Class(class) => {
for member in class.members.iter() {
if let ClassLikeMember::Method(method) = member {
if let MethodBody::Concrete(ref body) = method.body {
self.check_block(body);
}
}
}
}
Statement::Namespace(ns) => {
let statements = match &ns.body {
NamespaceBody::Implicit(body) => &body.statements,
NamespaceBody::BraceDelimited(body) => &body.statements,
};
self.check_statement_sequence(statements.as_slice());
}
Statement::Block(block) => self.check_block(block),
Statement::If(if_stmt) => self.check_if_body(&if_stmt.body),
Statement::While(while_stmt) => self.check_while_body(&while_stmt.body),
Statement::For(for_stmt) => self.check_for_body(&for_stmt.body),
Statement::Foreach(foreach_stmt) => self.check_foreach_body(&foreach_stmt.body),
Statement::Try(try_stmt) => {
self.check_block(&try_stmt.block);
for catch in try_stmt.catch_clauses.iter() {
self.check_block(&catch.block);
}
if let Some(finally) = &try_stmt.finally_clause {
self.check_block(&finally.block);
}
}
_ => {}
}
}
fn check_block(&mut self, block: &Block<'_>) {
self.check_statement_sequence(block.statements.as_slice());
}
fn check_statement_sequence(&mut self, statements: &[Statement<'_>]) {
for stmt in statements.iter() {
self.check_statement(stmt);
}
// Add pattern detection across adjacent statements here
}
// Helper methods for control flow bodies
fn check_if_body(&mut self, body: &IfBody<'_>) {
match body {
IfBody::Statement(stmt_body) => {
self.check_statement(stmt_body.statement);
for else_if in stmt_body.else_if_clauses.iter() {
self.check_statement(else_if.statement);
}
if let Some(else_clause) = &stmt_body.else_clause {
self.check_statement(else_clause.statement);
}
}
IfBody::ColonDelimited(block) => {
self.check_statement_sequence(block.statements.as_slice());
}
}
}
fn check_while_body(&mut self, body: &WhileBody<'_>) {
match body {
WhileBody::Statement(stmt) => self.check_statement(stmt),
WhileBody::ColonDelimited(block) => self.check_statement_sequence(block.statements.as_slice()),
}
}
fn check_for_body(&mut self, body: &ForBody<'_>) {
match body {
ForBody::Statement(stmt) => self.check_statement(stmt),
ForBody::ColonDelimited(block) => self.check_statement_sequence(block.statements.as_slice()),
}
}
fn check_foreach_body(&mut self, body: &ForeachBody<'_>) {
match body {
ForeachBody::Statement(stmt) => self.check_statement(stmt),
ForeachBody::ColonDelimited(block) => self.check_statement_sequence(block.statements.as_slice()),
}
}
}
pub struct {RuleName}Rule;
impl Rule for {RuleName}Rule {
fn name(&self) -> &'static str {
"{rule_name}"
}
fn description(&self) -> &'static str {
"{description}"
}
fn check<'a>(&self, program: &Program<'a>, source: &str) -> Vec<Edit> {
check_{rule_name}(program, source)
}
fn category(&self) -> Category {
Category::{category}
}
fn min_php_version(&self) -> Option<PhpVersion> {
Some(PhpVersion::{php_version})
}
}
#[cfg(test)]
mod tests {
use super::*;
use bumpalo::Bump;
use mago_database::file::FileId;
use rustor_core::apply_edits;
fn check_php(source: &str) -> Vec<Edit> {
let arena = Bump::new();
let file_id = FileId::new("test.php");
let (program, _) = mago_syntax::parser::parse_file_content(&arena, file_id, source);
check_{rule_name}(program, source)
}
fn transform(source: &str) -> String {
let edits = check_php(source);
apply_edits(source, &edits).unwrap()
}
#[test]
fn test_basic_pattern() {
let source = r#"<?php
{test_before}
"#;
let edits = check_php(source);
assert_eq!(edits.len(), 1);
let result = transform(source);
assert!(result.contains("{test_after}"));
}
// Add skip cases for patterns that should NOT match
#[test]
fn test_skip_wrong_pattern() {
let source = r#"<?php
{skip_case}
"#;
let edits = check_php(source);
assert_eq!(edits.len(), 0);
}
}
Step 3: Register the Rule
- Add to
lib.rs:
pub mod {rule_name};
pub use {rule_name}::check_{rule_name};
- Add to
registry.rs:
registry.register(Box::new(super::{rule_name}::{RuleName}Rule));
Step 4: Run Tests
cargo test -p rustor-rules {rule_name} -- --nocapture
AST Reference
Common AST Patterns
| PHP Pattern | AST Node | Access |
|---|---|---|
func() | Expression::Call(Call::Function) | call.argument.arguments |
$obj->method() | Expression::Call(Call::Method) | call.object, call.method |
$var | Expression::Variable(Variable::Direct) | var.name |
if (cond) {} | Statement::If | if_stmt.condition, if_stmt.body |
foreach ($a as $v) | Statement::Foreach | foreach.expression, foreach.target |
$a = $b | Expression::Assignment | assign.lhs, assign.rhs, assign.operator |
true/false | Expression::Literal(Literal::True/False) | direct match |
null | Expression::Literal(Literal::Null) | direct match |
Foreach Target
// Value only: foreach ($arr as $val)
let ForeachTarget::Value(value) = &foreach.target;
// Key-value: foreach ($arr as $key => $val)
let ForeachTarget::KeyValue(kv) = &foreach.target;
let key = &kv.key; // Expression
let value = &kv.value; // Expression
If Body Statements
fn get_if_body_statements<'a>(&self, body: &'a IfBody<'a>) -> Option<&'a [Statement<'a>]> {
match body {
IfBody::Statement(stmt_body) => {
match stmt_body.statement {
Statement::Block(ref block) => Some(block.statements.as_slice()),
_ => None,
}
}
IfBody::ColonDelimited(block) => Some(block.statements.as_slice()),
}
}
Helper Functions
// Get variable name without $
fn get_simple_variable_name(&self, expr: &Expression<'_>) -> Option<String> {
if let Expression::Variable(Variable::Direct(var)) = expr {
return Some(var.name.trim_start_matches('$').to_string());
}
None
}
// Check for literals
fn is_true_literal(&self, expr: &Expression<'_>) -> bool {
matches!(expr, Expression::Literal(Literal::True(_)))
}
fn is_false_literal(&self, expr: &Expression<'_>) -> bool {
matches!(expr, Expression::Literal(Literal::False(_)))
}
fn is_null_literal(&self, expr: &Expression<'_>) -> bool {
matches!(expr, Expression::Literal(Literal::Null(_)))
}
Categories and PHP Versions
| Category | Use For |
|---|---|
Performance | Runtime optimizations |
Modernization | New PHP syntax features |
Simplification | Cleaner code |
Compatibility | Best practices |
| PHP Version | Constant |
|---|---|
| 5.4 | Php54 |
| 7.0-7.4 | Php70 - Php74 |
| 8.0-8.4 | Php80 - Php84 |
| 8.5 | Php85 |
Multi-Statement Pattern Detection
For patterns spanning multiple statements (like foreach loops):
fn check_statement_sequence(&mut self, statements: &[Statement<'_>]) {
// First, recurse into each statement
for stmt in statements.iter() {
self.check_statement(stmt);
}
// Then check patterns across adjacent statements
for i in 0..statements.len() {
// Pattern: prev_stmt + current_stmt
if i > 0 {
if let Some(edit) = self.check_pattern(&statements[i - 1], &statements[i]) {
self.edits.push(edit);
}
}
// Pattern: current_stmt + next_stmt
if i + 1 < statements.len() {
if let Some(edit) = self.check_pattern(&statements[i], &statements[i + 1]) {
self.edits.push(edit);
}
}
}
}
Example: Foreach to Array Function
See these implemented rules for reference:
foreach_to_array_any.rs- Boolean assignment + early return patternsforeach_to_array_all.rs- Negation handlingforeach_to_array_find.rs- Null assignment patternforeach_to_array_find_key.rs- Key-value iteration
Execution Checklist
- Create rule file in
crates/rustor-rules/src/ - Add
pub mod {name};tolib.rs - Add
pub use {name}::check_{name};tolib.rs - Register in
registry.rs - Run
cargo test -p rustor-rules {name} - Run
cargo build --release