Skip to content

Alidmo/OpenLegacyGuard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenLegacyGuard

A static analysis tool that bridges legacy procedural PHP to modern Symfony / PSR-standard code using AST traversal and local LLM-assisted refactoring.


The Problem

Enterprise PHP codebases written in the early 2000s share a common fingerprint:

// ❌ The enemy
global $db_connection;
$result = mysql_query("SELECT * FROM users WHERE id = $id", $db_connection);

These patterns — global variables, mysql_* calls, God Functions with cyclomatic complexity in the 30s — are not just ugly. They are actively dangerous: untestable, SQL-injection-prone, and impossible to migrate to modern frameworks without a systematic approach.

OpenLegacyGuard automates the detection step and bridges the gap to the refactor step using a local LLM.


How It Works

┌──────────────┐     ┌──────────────────┐     ┌─────────────────────┐     ┌─────────────┐
│  PHP Source  │────▶│  nikic/php-parser │────▶│  NodeTraverser      │────▶│  ScanReport │
│  Files       │     │  (AST generation) │     │  + Rule Visitors    │     │  Violations │
└──────────────┘     └──────────────────┘     └─────────────────────┘     └──────┬──────┘
                                                                                  │
                                                                    ┌─────────────▼──────────────┐
                                                                    │  ConsoleReporter           │
                                                                    │  (Colored table output)    │
                                                                    └─────────────┬──────────────┘
                                                                                  │  --refactor
                                                                    ┌─────────────▼──────────────┐
                                                                    │  AiRefactorService         │
                                                                    │  (Ollama HTTP API)         │
                                                                    └─────────────┬──────────────┘
                                                                                  │
                                                                    ┌─────────────▼──────────────┐
                                                                    │  DiffRenderer              │
                                                                    │  (Legacy RED / Modern GREEN│
                                                                    └────────────────────────────┘

Why the Visitor Pattern?

nikic/php-parser's NodeTraverser accepts multiple NodeVisitor instances and dispatches every node to all of them in a single O(n) pass. Each rule is a self-contained visitor class — adding a new rule is a three-line change with zero impact on existing rules. This is the Open/Closed Principle in action.


Installation

Requirements: PHP 8.3+, Composer, Ollama (for --refactor mode)

git clone <repo>
cd OpenLegacyGuard
composer install
chmod +x bin/guard

Install Ollama (for AI refactoring):

# macOS / Linux
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3
ollama serve

Usage

Scan a directory

./bin/guard scan /var/www/legacy/src

Sample output:

OpenLegacyGuard — scanning /var/www/legacy/src

 12/12 [============================] 100% — Done

  3 smell(s) found across 2 file(s)

  src/helpers/user.php
   Line  Rule                      Message
   12    Global Variable Usage     Use of `global` keyword detected ($db_connection, $current_user)...
   18    Global Variable Usage     Use of `global` keyword detected ($db_connection)...

  src/controllers/order.php
   48    High Complexity / God Fn  Function `processOrder()` is a God Function: cyclomatic complexity 22...

Scan and request AI refactoring

./bin/guard scan /var/www/legacy/src --refactor

After the scan, you are prompted to select a violation:

 Select a violation to refactor (or "Exit"):
  [1] Global Variable Usage — user.php:12
  [2] Global Variable Usage — user.php:18
  [3] High Complexity / God Function — order.php:48
  [4] Exit

Press [1] and wait for Ollama:

  ──── LEGACY ────────────────────────────────────────────
  − 1 │ global $db_connection, $current_user;
  − 2 │ $result = mysql_query("SELECT * FROM users WHERE id = $id");

  ──── MODERN (Suggested Refactor) ──────────────────────
  + 1 │ <?php
  + 2 │ final class UserRepository extends ServiceEntityRepository
  + 3 │ {
  + 4 │     public function __construct(ManagerRegistry $registry)
  + 5 │     {
  + 6 │         parent::__construct($registry, User::class);
  + 7 │     }
  ...

CI/CD mode (no progress bar, non-zero exit on violations)

./bin/guard scan src/ --no-progress
echo $?  # 0 = clean, 1 = violations found

Architecture

src/
├── Ai/
│   ├── AiRefactorException.php         # Base exception for all Ollama errors
│   ├── AiRefactorService.php           # HTTP client wrapper for Ollama API
│   ├── OllamaTimeoutException.php      # LLM inference timeout
│   ├── OllamaUnavailableException.php  # Ollama not running
│   └── RefactorPromptBuilder.php       # Builds rule-specific system + user prompts
├── Analyzer/
│   └── AstAnalyzer.php                 # Parses files → traverses AST → stamps violations
├── Command/
│   └── ScanCommand.php                 # Symfony Console command: scan + interactive refactor
├── Diff/
│   └── DiffRenderer.php                # Colored Legacy/Modern diff in terminal
├── Report/
│   ├── ConsoleReporter.php             # Renders ScanReport to Symfony Console
│   ├── ScanReport.php                  # Aggregates Violations; groupByFile/groupByRule
│   └── Violation.php                   # Immutable value object: ruleName/filePath/line/message
├── Rules/
│   ├── AbstractRule.php                # NodeVisitorAbstract base; addViolation() helper
│   ├── ComplexityDetector.php          # Flags God Functions by line count + cyclomatic complexity
│   ├── GlobalUsageDetector.php         # Detects `global` keyword usage
│   ├── ProceduralSqlDetector.php       # Detects mysql_*, raw PDO->query(), raw SQL strings
│   ├── RuleInterface.php               # Contract: getName/getDescription/getViolations/reset
│   └── RulesRegistry.php              # Central registry: RulesRegistry::all()
└── Scanner/
    └── FileScanner.php                 # Symfony Finder wrapper; recursive .php discovery

Adding a New Rule

  1. Create src/Rules/YourNewDetector.php:
<?php
declare(strict_types=1);

namespace OpenLegacyGuard\Rules;

use PhpParser\Node;

final class YourNewDetector extends AbstractRule
{
    public function getName(): string { return 'Your Rule Name'; }
    public function getDescription(): string { return 'What it detects.'; }

    public function enterNode(Node $node): null
    {
        if ($node instanceof Node\Stmt\Echo_) {
            $this->addViolation(
                line: (int) $node->getStartLine(),
                message: 'Use of echo in non-template context.',
            );
        }
        return null;
    }
}
  1. Register in src/Rules/RulesRegistry.php:
return [
    new GlobalUsageDetector(),
    new ProceduralSqlDetector(),
    new ComplexityDetector(),
    new YourNewDetector(),  // ← add here
];
  1. Write a PHPUnit test:
touch tests/Rules/YourNewDetectorTest.php
./vendor/bin/phpunit tests/Rules/YourNewDetectorTest.php

Done. Zero changes to existing code.


Testing

composer install
./vendor/bin/phpunit

Test coverage includes:

  • Unit tests for each rule against dedicated fixture files
  • Negative testsclean_example.php must produce zero violations
  • Reset tests — rules must not accumulate state across files
  • Integration tests — full scanner → analyzer → report pipeline
  • AI service tests — mock HTTP client, all failure modes exercised

Supported Legacy Smells

Rule What it detects Modern alternative
GlobalUsageDetector global $var inside functions Constructor injection / Symfony Service
ProceduralSqlDetector mysql_*, raw PDO->query(), raw SQL strings Doctrine ORM / PDO prepared statements
ComplexityDetector Functions > 50 lines or cyclomatic complexity > 15 Single-responsibility methods / Services

Configuring Ollama

Variable Default Description
Base URL http://localhost:11434 Change if Ollama runs on a different host/port
Model llama3 Any model pulled via ollama pull <model>
Timeout 120s Increase for large snippets on CPU-only machines

Exit Codes

Code Meaning
0 No violations found — codebase is clean
1 One or more violations detected

This makes guard scan safe to use as a CI gate.


License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors