diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml new file mode 100644 index 0000000000..77eabec0e6 --- /dev/null +++ b/.github/workflows/phpstan.yaml @@ -0,0 +1,101 @@ +name: PHPStan + +on: + push: + branches: [ alpha ] + pull_request: + branches: [ alpha ] + +jobs: + phpstan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP 7.4 for dependencies + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + + - name: Install Dependencies + run: composer update --ignore-platform-reqs + + - name: Setup PHP 8.2 for PHPStan + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + + - name: Restore PHPStan cache + id: cache-phpstan + uses: actions/cache/restore@v3 + with: + path: phpstan.phar + key: phpstan-1 + + - name: Download PHPStan + if: steps.cache-phpstan.outputs.cache-hit != 'true' + run: wget https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar + + - name: Run PHPStan + run: php phpstan.phar analyse --configuration phpstan.neon + + update-baseline: + needs: phpstan + if: github.event_name == 'push' && github.ref == 'refs/heads/alpha' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Delete existing branch if exists + run: | + if git ls-remote --heads origin update-phpstan-baseline | grep update-phpstan-baseline; then + git push origin --delete update-phpstan-baseline + fi + + - name: Setup PHP 7.4 for dependencies + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + + - name: Install Dependencies + run: composer update --ignore-platform-reqs + + - name: Setup PHP 8.2 for PHPStan + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + + - name: Download PHPStan + run: wget https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar + + - name: Generate new baseline + id: generate-baseline + run: | + cp phpstan-baseline.neon phpstan-baseline.neon.old + php phpstan.phar analyse --configuration phpstan.neon --generate-baseline + if ! diff -q phpstan-baseline.neon phpstan-baseline.neon.old > /dev/null; then + echo "baseline_changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.generate-baseline.outputs.baseline_changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + commit-message: Update PHPStan baseline + title: '[CI] Update PHPStan baseline' + body: | + Mise à jour automatique du baseline PHPStan suite à la correction d'erreurs. + + Cette PR a été générée automatiquement par le workflow CI/CD. + branch: update-phpstan-baseline + base: alpha + delete-branch: true + reviewers: | + zoic21 + Hotfirenet + tmartinez69009 + Salvialf + Sekiro-kost + reineabs diff --git a/docs/fr_FR/static-analysis/phpstan.md b/docs/fr_FR/static-analysis/phpstan.md new file mode 100644 index 0000000000..91788ab33b --- /dev/null +++ b/docs/fr_FR/static-analysis/phpstan.md @@ -0,0 +1,142 @@ +# Guide PHPStan pour Jeedom + +## Installation locale + +1. Télécharger PHPStan : +```bash +wget https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar +``` + +2. Mettre à jour les dépendances : +```bash +composer update --ignore-platform-reqs +``` + +## Configuration + +Le fichier `phpstan.neon` à la racine du projet contient la configuration suivante : + +```yaml +parameters: + level: 1 + paths: + - core + - desktop + - install + - mobile + excludePaths: + - vendor/* + tmpDir: .phpstan.cache + baseline: phpstan-baseline.neon + reportUnmatchedIgnoredErrors: false + +includes: + - phpstan-baseline.neon +``` + +Notes importantes : +- Niveau d'analyse : 1 / 10 (0 = minimum, 10 = maximum) +- Le baseline permet d'ignorer les erreurs existantes +- Les erreurs corrigées sont automatiquement retirées des erreurs détectées + +## Utilisation quotidienne + +### Lancer l'analyse +```bash +php phpstan.phar analyse --configuration phpstan.neon +``` + +### Types d'erreurs courantes et solutions + +1. **Variable might not be defined** : +```php +// Erreur +function maFonction() { + if($condition) { + $variable = 'valeur'; + } + echo $variable; // Erreur : variable non définie si condition fausse +} + +// Solution +function maFonction() { + $variable = null; // Initialisation par défaut + if($condition) { + $variable = 'valeur'; + } + echo $variable; +} +``` + +2. **Method X not found in class Y** : +```php +// Erreur +$object->methodQuiNexistePas(); + +// Solution +// Vérifier si la méthode existe dans la classe +// Ou utiliser une interface/classe abstraite pour définir le contrat +``` + +3. **Cannot call method X on mixed** : +```php +// Erreur +$result = getData(); // getData() retourne mixed +$result->method(); // Erreur : impossible d'appeler une méthode sur mixed + +// Solution +if (is_object($result)) { + $result->method(); +} +``` + +## Cas particuliers + +### Ignorer une erreur spécifique +Si une erreur ne peut pas être corrigée ou doit être ignorée, ajoutez un commentaire PHPStan : +```php +/** @phpstan-ignore-next-line */ +$resultat = codeProblematiqueQuiNeDoitPasEtreModifie(); +``` + +### Générer un nouveau baseline +Si de nombreuses erreurs existantes doivent être ignorées : +```bash +php phpstan.phar analyse --configuration phpstan.neon --generate-baseline +``` + +## Intégration continue + +### Vérification du code + +Le workflow GitHub Actions vérifie automatiquement le code à chaque push et pull request sur la branche alpha. En cas d'échec : + +1. Consultez les logs de l'action pour voir les erreurs +2. Reproduisez l'analyse en local +3. Corrigez les erreurs ou mettez à jour le baseline si nécessaire + +### Mise à jour automatique du baseline + +Un processus automatique a été mis en place pour maintenir le baseline à jour : + +1. Après chaque merge sur alpha, le système vérifie si des erreurs du baseline peuvent être supprimées +2. Si des erreurs ont été corrigées et peuvent être retirées du baseline : + - Une nouvelle branche `update-phpstan-baseline` est créée + - Une Pull Request est automatiquement ouverte + - La PR contient uniquement la mise à jour du fichier `phpstan-baseline.neon` +3. Cette PR peut être revue et mergée comme toute autre PR + +👉 Note : Il n'est pas nécessaire de mettre à jour le baseline manuellement, le système automatique s'en charge lorsque des erreurs sont corrigées. + +## Bonnes pratiques + +- Exécutez PHPStan localement avant de commiter +- Corrigez les erreurs plutôt que de les ignorer quand c'est possible +- Pour les nouvelles classes/méthodes, essayez de ne pas générer de nouvelles erreurs +- Commentez le code clairement quand vous devez ignorer une erreur +- Laissez le système automatique gérer la mise à jour du baseline +- Revoyez les PRs de mise à jour du baseline pour vérifier que les erreurs retirées ont bien été corrigées intentionnellement + +--- + +Besoin d'aide supplémentaire ? Consultez la [documentation officielle de PHPStan](https://phpstan.org/user-guide/getting-started). \ No newline at end of file diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000000..f4773345ab --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,469 @@ +parameters: + ignoreErrors: + - + message: '#^Static method cache\:\:set\(\) invoked with 4 parameters, 2\-3 required\.$#' + identifier: arguments.count + count: 1 + path: core/ajax/cache.ajax.php + + - + message: '#^Static call to instance method cmd\:\:dropInfluxDatabase\(\)\.$#' + identifier: method.staticCall + count: 1 + path: core/ajax/cmd.ajax.php + + - + message: '#^Static call to instance method cmd\:\:historyInfluxAll\(\)\.$#' + identifier: method.staticCall + count: 1 + path: core/ajax/cmd.ajax.php + + - + message: '#^Static method history\:\:getHistoryFromCalcul\(\) invoked with 6 parameters, 1\-5 required\.$#' + identifier: arguments.count + count: 1 + path: core/ajax/cmd.ajax.php + + - + message: '#^Variable \$eqLogic might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/ajax/eqLogic.ajax.php + + - + message: '#^Static method interactQuery\:\:byInteractDefId\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 2 + path: core/ajax/interact.ajax.php + + - + message: '#^Static method listener\:\:all\(\) invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count + count: 1 + path: core/ajax/listener.ajax.php + + - + message: '#^Static method jeeObject\:\:getUISelectList\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 1 + path: core/ajax/object.ajax.php + + - + message: '#^Static method queue\:\:all\(\) invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count + count: 1 + path: core/ajax/queue.ajax.php + + - + message: '#^Variable \$market might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/ajax/repo.ajax.php + + - + message: '#^Variable \$return might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/ajax/report.ajax.php + + - + message: '#^Variable \$cmd might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/ajax/scenario.ajax.php + + - + message: '#^Variable \$converts might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: core/ajax/scenario.ajax.php + + - + message: '#^Result of static method user\:\:removeBanIp\(\) \(void\) is used\.$#' + identifier: staticMethod.void + count: 1 + path: core/ajax/user.ajax.php + + - + message: '#^Variable \$configs might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/ajax/user.ajax.php + + - + message: '#^Static method jeedom\:\:update\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 2 + path: core/api/jeeApi.php + + - + message: '#^Static method log\:\:getDelta\(\) invoked with 8 parameters, 0\-7 required\.$#' + identifier: arguments.count + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$cmd might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$jsonrpc in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$object might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$request in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$scenario might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/api/jeeApi.php + + - + message: '#^Variable \$update might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: core/api/jeeApi.php + + - + message: '#^Call to static method byId\(\) on an unknown class jeeNetwork\.$#' + identifier: class.notFound + count: 1 + path: core/api/proApi.php + + - + message: '#^Call to static method byId\(\) on an unknown class market\.$#' + identifier: class.notFound + count: 2 + path: core/api/proApi.php + + - + message: '#^Call to static method byLogicalId\(\) on an unknown class market\.$#' + identifier: class.notFound + count: 1 + path: core/api/proApi.php + + - + message: '#^Call to static method testMaster\(\) on an unknown class jeeNetwork\.$#' + identifier: class.notFound + count: 1 + path: core/api/proApi.php + + - + message: '#^Static method jeedom\:\:update\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 2 + path: core/api/proApi.php + + - + message: '#^Static method log\:\:getDelta\(\) invoked with 8 parameters, 0\-7 required\.$#' + identifier: arguments.count + count: 1 + path: core/api/proApi.php + + - + message: '#^Static method repo_market\:\:backup_restore\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: core/api/proApi.php + + - + message: '#^Instantiated class InfluxDB\\Client not found\.$#' + identifier: class.notFound + count: 2 + path: core/class/cmd.class.php + + - + message: '#^Instantiated class InfluxDB\\Point not found\.$#' + identifier: class.notFound + count: 2 + path: core/class/cmd.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 2 + path: core/class/config.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 1 + path: core/class/eqLogic.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 3 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 1 + path: core/class/event.class.php + + - + message: '#^Variable \$goupingType might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: core/class/history.class.php + + - + message: '#^Static call to instance method interactQuery\:\:replaceForContextual\(\)\.$#' + identifier: method.staticCall + count: 3 + path: core/class/interactQuery.class.php + + - + message: '#^Variable \$closest might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/interactQuery.class.php + + - + message: '#^Variable \$cmd might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/interactQuery.class.php + + - + message: '#^Variable \$objects might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/interactQuery.class.php + + - + message: '#^Instantiated class virtual not found\.$#' + identifier: class.notFound + count: 2 + path: core/class/jeeObject.class.php + + - + message: '#^Instantiated class virtualCmd not found\.$#' + identifier: class.notFound + count: 4 + path: core/class/jeeObject.class.php + + - + message: '#^Variable \$events might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/jeeObject.class.php + + - + message: '#^Variable \$ch might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: core/class/jsonrpcClient.class.php + + - + message: '#^Variable \$http_status might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/jsonrpcClient.class.php + + - + message: '#^Variable \$response might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: core/class/jsonrpcClient.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 1 + path: core/class/message.class.php + + - + message: '#^Instantiated class openvpn not found\.$#' + identifier: class.notFound + count: 1 + path: core/class/network.class.php + + - + message: '#^Variable \$listPlugin in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: core/class/plugin.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 2 + path: core/class/scenario.class.php + + - + message: '#^Variable \$return might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/scenario.class.php + + - + message: '#^Instantiated class SolarData\\SolarData not found\.$#' + identifier: class.notFound + count: 1 + path: core/class/scenarioExpression.class.php + + - + message: '#^Static method event\:\:add\(\) invoked with 2 parameters, 0\-1 required\.$#' + identifier: arguments.count + count: 6 + path: core/class/scenarioExpression.class.php + + - + message: '#^Undefined variable\: \$_parameters$#' + identifier: variable.undefined + count: 8 + path: core/class/scenarioExpression.class.php + + - + message: '#^Variable \$_parameters in isset\(\) is never defined\.$#' + identifier: isset.variable + count: 4 + path: core/class/scenarioExpression.class.php + + - + message: '#^Static method update\:\:getLastAvailableVersion\(\) invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count + count: 1 + path: core/class/update.class.php + + - + message: '#^Variable \$url might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/update.class.php + + - + message: '#^Variable \$return might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/class/utils.class.php + + - + message: '#^Access to an undefined property widgets\:\:\$_needRefreshWidget\.$#' + identifier: property.notFound + count: 1 + path: core/class/widgets.class.php + + - + message: '#^Array has 2 duplicate keys with value ''THERMOSTAT_HUMIDITY'' \(''THERMOSTAT_HUMIDITY'', ''THERMOSTAT_HUMIDITY''\)\.$#' + identifier: array.duplicateKey + count: 1 + path: core/config/jeedom.config.php + + - + message: '#^Instantiated class elFinder not found\.$#' + identifier: class.notFound + count: 1 + path: core/php/editor.connector.php + + - + message: '#^Instantiated class elFinderConnector not found\.$#' + identifier: class.notFound + count: 1 + path: core/php/editor.connector.php + + - + message: '#^Result of function header \(void\) is used\.$#' + identifier: function.void + count: 1 + path: core/php/utils.inc.php + + - + message: '#^Variable \$matches might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/php/utils.inc.php + + - + message: '#^Variable \$return might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/php/utils.inc.php + + - + message: '#^Inner named functions are not supported by PHPStan\. Consider refactoring to an anonymous function, class method, or a top\-level\-defined function\. See issue \#165 \(https\://github\.com/phpstan/phpstan/issues/165\) for more details\.$#' + identifier: function.inner + count: 1 + path: core/repo/market.repo.php + + - + message: '#^Variable \$files might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/repo/market.repo.php + + - + message: '#^Variable \$found might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: core/repo/market.repo.php + + - + message: '#^Variable \$link might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: desktop/modal/planHeader.configure.php + + - + message: '#^Undefined variable\: \$plugin_enable$#' + identifier: variable.undefined + count: 2 + path: desktop/php/display.php + + - + message: '#^Variable \$plugin_enable in isset\(\) is never defined\.$#' + identifier: isset.variable + count: 2 + path: desktop/php/display.php + + - + message: '#^Variable \$code might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: desktop/php/eqAnalyse.php + + - + message: '#^Variable \$homeLink might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: desktop/php/index.php + + - + message: '#^Variable \$dataUrl might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: desktop/php/overview.php + + - + message: '#^Variable \$sessions might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: desktop/php/user.php + + - + message: '#^Variable \$plugin_exclude in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: install/backup.php + + - + message: '#^Variable \$plugin_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: install/restore.php + + - + message: '#^Variable \$cmd might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: install/update/reloadCache.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000..5daa84cc78 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,14 @@ +parameters: + level: 1 + paths: + - core + - desktop + - install + - mobile + excludePaths: + - vendor/* + tmpDir: .phpstan.cache + reportUnmatchedIgnoredErrors: false + +includes: + - phpstan-baseline.neon