CodeIgniter icon indicating copy to clipboard operation
CodeIgniter copied to clipboard

Tool - Dynamically Declared Properties Auto Detection

Open juanluislopez89 opened this issue 1 year ago • 1 comments

Hi! i Just made this controller to analyze wheter a system class has dynamically declared properties. Ithink this may help to adapt CI3 to the neew PHP 8.3 without using [#AllowDynamicProperties] argument that can be depecated in future versions.

Cannot detect if a property is inherited! so in some cases, there can be false positives.

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Analyze_dynamic_properties extends CI_Controller {

    public function __construct()
    {
        parent::__construct();
    }

    public function index()
    {
        $directory = SYSDIR;
        $dynamicProperties = $this->findDynamicProperties($directory);

        // Renderizar la vista directamente aquí
        echo $this->renderView($dynamicProperties);
    }

    private function findDynamicProperties($dir)
    {
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
        $dynamicProperties = [];

        foreach ($files as $file) {
            if ($file->isDir() || $file->getExtension() !== 'php') {
                continue;
            }

            $content = file_get_contents($file->getRealPath());
            $tokens = token_get_all($content);
            $classes = [];
            $currentClass = '';
            $collectingClass = false;
            $declaredProperties = [];

            for ($i = 0; $i < count($tokens); $i++) {
                $token = $tokens[$i];

                if (is_array($token)) {
                    if ($token[0] === T_CLASS) {
                        $collectingClass = true;
                    } elseif ($collectingClass && $token[0] === T_STRING) {
                        $currentClass = $token[1];
                        $classes[$currentClass] = ['file' => $file->getRealPath(), 'properties' => [], 'declared' => []];
                        $collectingClass = false;
                    } elseif ($currentClass && $token[0] === T_VARIABLE && $token[1] === '$this') {
                        $nextToken = $tokens[$i + 1] ?? null;
                        if (is_array($nextToken) && $nextToken[0] === T_OBJECT_OPERATOR) {
                            $propertyToken = $tokens[$i + 2] ?? null;
                            $assignmentToken = $tokens[$i + 4] ?? null;
                            if (is_array($propertyToken) && $propertyToken[0] === T_STRING && $assignmentToken === '=') {
                                $propertyName = $propertyToken[1];
                                if (!in_array($propertyName, $declaredProperties) && !in_array($propertyName, $classes[$currentClass]['declared'])) {
                                    $classes[$currentClass]['properties'][] = [
                                        'name' => $propertyName,
                                        'declared' => false
                                    ];
                                }
                            }
                        }
                    } elseif ($currentClass && $token[0] === T_VARIABLE) {
                        $declaredProperties[] = substr($token[1], 1);
                    } elseif ($currentClass && $token[0] === T_PUBLIC) {
                        $nextToken = $tokens[$i + 2] ?? null;
                        if (is_array($nextToken) && $nextToken[0] === T_VARIABLE) {
                            $declaredPropertyName = substr($nextToken[1], 1);
                            $classes[$currentClass]['declared'][] = $declaredPropertyName;
                            $classes[$currentClass]['properties'][] = [
                                'name' => $declaredPropertyName,
                                'declared' => true
                            ];
                        }
                    }
                }
            }

            foreach ($classes as $className => $data) {
                if (!empty($data['properties'])) {
                    $dynamicProperties[$className]['file'] = $data['file'];
                    $dynamicProperties[$className]['properties'] = $data['properties'];
                }
            }
        }

        return $dynamicProperties;
    }

    private function renderView($dynamicProperties)
    {
        ob_start();
        ?>
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>PHP 8 Compatibility Analysis</title>
            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
            <style>
                .table td.success {
                    background-color: #dff0d8;
                }
                .table td.danger {
                    background-color: #f2dede;
                }
            </style>
        </head>
        <body>
        <div class="container">
            <h1>PHP 8 Compatibility Analysis</h1><hr>
            <div class="btn-group" role="group" aria-label="Filter Buttons">
                <button type="button" class="btn btn-success" onclick="filterClasses('good')">Good Classes</button>
                <button type="button" class="btn btn-danger" onclick="filterClasses('bad')">Wrong Classes</button>
                <button type="button" class="btn btn-primary" onclick="filterClasses('all')">All Classes</button>
            </div>
            <hr>
            <table class="table table-bordered table-striped" id="resultsTable">
                <thead>
                <tr>
                    <th>Class</th>
                    <th>File</th>
                    <th>Property</th>
                    <th>Status</th>
                </tr>
                </thead>
                <tbody>
                <?php foreach ($dynamicProperties as $className => $data): ?>
                    <?php 
                    $classStatus = 'good'; 
                    foreach ($data['properties'] as $property): 
                        if (!$property['declared']) {
                            $classStatus = 'bad';
                            break;
                        }
                    endforeach;
                    ?>
                    <?php foreach ($data['properties'] as $index => $property): ?>
                    <tr class="<?= $classStatus ?>">
                        <?php if ($index === 0): ?>
                            <td rowspan="<?= count($data['properties']) ?>"><?= $className ?></td>
                            <td rowspan="<?= count($data['properties']) ?>"><?= $data['file'] ?></td>
                        <?php endif; ?>
                        <td class="<?= $property['declared'] ? 'success' : 'danger' ?>"><?= $property['name'] ?></td>
                        <td class="<?= $property['declared'] ? 'success' : 'danger' ?>">
                            <strong class="text-<?= $property['declared'] ? 'success' : 'danger' ?>"><?= $property['declared'] ? 'Declared =)' : 'Not Declared !!!' ?></strong>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                <?php endforeach; ?>
                </tbody>
            </table>
        </div>

        <script>
            function filterClasses(status) {
                var rows = document.querySelectorAll('#resultsTable tbody tr');
                rows.forEach(function (row) {
                    if (status === 'all') {
                        row.style.display = '';
                    } else if (status === 'good' && row.classList.contains('good')) {
                        row.style.display = '';
                    } else if (status === 'bad' && row.classList.contains('bad')) {
                        row.style.display = '';
                    } else {
                        row.style.display = 'none';
                    }
                });
            }
        </script>
        </body>
        </html>
        <?php
        return ob_get_clean();
    }
}
`

juanluislopez89 avatar Aug 13 '24 10:08 juanluislopez89

Sadly this doesn't work and reports many false positives.

For example in the attached image:

image

CI_Session_redis_driver extends CI_Session_driver and all of the properties listed as not being declared are in fact declared in the CI_Session_driver class and therefore are not dynamically declared.

I like the idea but the code needs work.

daveherman71 avatar Aug 15 '24 08:08 daveherman71