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
40 changes: 40 additions & 0 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: PHPUnit

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
phpunit:
name: Run PHPUnit
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: json, curl
coverage: none
tools: composer:v2

- name: Cache Composer dependencies
uses: actions/cache@v5
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Run PHPUnit
run: composer test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ composer.lock
.phpunit.cache/
coverage-report/
.php-cs-fixer.cache
/htmlcov/
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"check": [
"@phpcs",
"@phpstan",
"@rector"
"@rector",
"@test"
],
"fix": [
"@phpcbf",
Expand All @@ -62,8 +63,10 @@
"phpcbf": "phpcbf",
"phpcs": "phpcs",
"phpstan": "phpstan analyse -c phpstan.neon src",
"phpunit": "phpunit --testdox",
"phpunit-coverage": "phpunit --coverage-html htmlcov --coverage-text",
"rector": "rector process --dry-run",
"rector-fix": "rector process",
"test": "phpunit"
"test": "@phpunit"
}
}
6 changes: 5 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerErrors="true"
stderr="true"
beStrictAboutOutputDuringTests="false">
beStrictAboutOutputDuringTests="false"
failOnWarning="false">
<testsuites>
<testsuite name="Unit Tests">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Rules">
<directory>tests/Rules</directory>
</testsuite>
</testsuites>

<source>
Expand Down
25 changes: 25 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerErrors="true"
stderr="true"
beStrictAboutOutputDuringTests="false"
failOnWarning="false">
<testsuites>
<testsuite name="Rules">
<directory>tests/Rules</directory>
</testsuite>
</testsuites>

<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
34 changes: 14 additions & 20 deletions src/Rules/Module/ControllersMustReturnResponseRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace OpenCoreEMR\PHPStan\Rules\Module;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
Expand Down Expand Up @@ -72,26 +73,19 @@ public function processNode(Node $node, Scope $scope): array
];
}

// Check if return type is void
$function = $scope->getFunction();
if ($function instanceof \PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection) {
$variants = $function->getVariants();
if (count($variants) > 0) {
$returnType = $variants[0]->getReturnType();
if ($returnType->isVoid()->yes()) {
return [
RuleErrorBuilder::message(
sprintf(
'Controller method %s() must return Response object, not void.',
$methodName
)
)
->identifier('openemr.controllerMustReturnResponse')
->tip('Controllers should return Response, JsonResponse, RedirectResponse, or BinaryFileResponse')
->build()
];
}
}
// Check if return type is void (using AST to avoid reflection issues in tests)
if ($node->returnType instanceof Identifier && $node->returnType->name === 'void') {
return [
RuleErrorBuilder::message(
sprintf(
'Controller method %s() must return Response object, not void.',
$methodName
)
)
->identifier('openemr.controllerMustReturnResponse')
->tip('Controllers should return Response, JsonResponse, RedirectResponse, or BinaryFileResponse')
->build()
];
}

return [];
Expand Down
34 changes: 34 additions & 0 deletions tests/RuleTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* Base test case for PHPStan rules
*
* @package OpenCoreEMR
* @link https://opencoreemr.com
* @author Michael A. Smith <michael@opencoreemr.com>
* @copyright Copyright (c) 2026 OpenCoreEMR Inc
* @license https://github.com/openCoreEMR/openemr-phpstan-rules/blob/main/LICENSE GNU General Public License 3
*/

namespace OpenCoreEMR\PHPStan\Tests;

use PHPStan\Testing\RuleTestCase as PHPStanRuleTestCase;

/**
* Base test case that includes test-specific configuration.
*
* @template TRule of \PHPStan\Rules\Rule
* @extends PHPStanRuleTestCase<TRule>
*/
abstract class RuleTestCase extends PHPStanRuleTestCase
{
/**
* @return string[]
*/
public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/phpstan-tests.neon',
];
}
}
55 changes: 55 additions & 0 deletions tests/Rules/Database/ForbiddenClassesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/**
* Test for ForbiddenClassesRule
*
* @package OpenCoreEMR
* @link https://opencoreemr.com
* @author Michael A. Smith <michael@opencoreemr.com>
* @copyright Copyright (c) 2026 OpenCoreEMR Inc
* @license https://github.com/openCoreEMR/openemr-phpstan-rules/blob/main/LICENSE GNU General Public License 3
*/

namespace OpenCoreEMR\PHPStan\Tests\Rules\Database;

use OpenCoreEMR\PHPStan\Rules\Database\ForbiddenClassesRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ForbiddenClassesRule>
*/
class ForbiddenClassesRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new ForbiddenClassesRule();
}

public function testForbiddenClasses(): void
{
$tip = 'See src/Common/Database/QueryUtils.php for modern database patterns';
$this->analyse([__DIR__ . '/data/forbidden-classes.php'], [
[
'Laminas-DB class "Laminas\Db\Adapter\Adapter" is deprecated. Use QueryUtils or DatabaseQueryTrait instead.',
5,
$tip,
],
[
'Laminas-DB class "Laminas\Db\Sql\Select" is deprecated. Use QueryUtils or DatabaseQueryTrait instead.',
6,
$tip,
],
[
'Laminas-DB class "Laminas\Db\TableGateway\TableGateway" is deprecated. Use QueryUtils or DatabaseQueryTrait instead.',
7,
$tip,
],
]);
}

public function testAllowedClasses(): void
{
$this->analyse([__DIR__ . '/data/allowed-classes.php'], []);
}
}
75 changes: 75 additions & 0 deletions tests/Rules/Database/ForbiddenFunctionsRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/**
* Test for ForbiddenFunctionsRule
*
* @package OpenCoreEMR
* @link https://opencoreemr.com
* @author Michael A. Smith <michael@opencoreemr.com>
* @copyright Copyright (c) 2026 OpenCoreEMR Inc
* @license https://github.com/openCoreEMR/openemr-phpstan-rules/blob/main/LICENSE GNU General Public License 3
*/

namespace OpenCoreEMR\PHPStan\Tests\Rules\Database;

use OpenCoreEMR\PHPStan\Rules\Database\ForbiddenFunctionsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ForbiddenFunctionsRule>
*/
class ForbiddenFunctionsRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new ForbiddenFunctionsRule();
}

public function testForbiddenFunctions(): void
{
$tip = 'Or use DatabaseQueryTrait in your class';
$this->analyse([__DIR__ . '/data/forbidden-functions.php'], [
[
'Use QueryUtils::querySingleRow() or QueryUtils::fetchRecords() instead of sqlQuery().',
5,
$tip,
],
[
'Use QueryUtils::sqlStatementThrowException() or QueryUtils::fetchRecords() instead of sqlStatement().',
6,
$tip,
],
[
'Use QueryUtils::sqlInsert() instead of sqlInsert().',
7,
$tip,
],
[
'Use QueryUtils::fetchRecords() or QueryUtils::fetchArrayFromResultSet() instead of sqlFetchArray().',
8,
$tip,
],
[
'Use QueryUtils::startTransaction() instead of sqlBeginTrans().',
9,
$tip,
],
[
'Use QueryUtils::commitTransaction() instead of sqlCommitTrans().',
10,
$tip,
],
[
'Use QueryUtils::rollbackTransaction() instead of sqlRollbackTrans().',
11,
$tip,
],
]);
}

public function testAllowedFunctions(): void
{
$this->analyse([__DIR__ . '/data/allowed-functions.php'], []);
}
}
7 changes: 7 additions & 0 deletions tests/Rules/Database/data/allowed-classes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

// Test file for ForbiddenClassesRule - these should NOT trigger errors

use Laminas\Form\Form;
use Laminas\Validator\StringLength;
use OpenEMR\Common\Database\QueryUtils;
12 changes: 12 additions & 0 deletions tests/Rules/Database/data/allowed-functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

// Test file for ForbiddenFunctionsRule - these should NOT trigger errors

// Regular function calls that are allowed
strlen('test');
array_map(fn($x) => $x * 2, [1, 2, 3]);
json_encode(['key' => 'value']);

// Method calls are allowed (only global functions are forbidden)
$db->sqlQuery('SELECT * FROM users');
$utils->sqlStatement('UPDATE users SET name = ?', ['John']);
7 changes: 7 additions & 0 deletions tests/Rules/Database/data/forbidden-classes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

// Test file for ForbiddenClassesRule - these imports should trigger errors

use Laminas\Db\Adapter\Adapter;
use Laminas\Db\Sql\Select;
use Laminas\Db\TableGateway\TableGateway;
11 changes: 11 additions & 0 deletions tests/Rules/Database/data/forbidden-functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

// Test file for ForbiddenFunctionsRule - these should all trigger errors

sqlQuery('SELECT * FROM users');
sqlStatement('UPDATE users SET name = ?', ['John']);
sqlInsert('INSERT INTO users (name) VALUES (?)', ['Jane']);
sqlFetchArray($result);
sqlBeginTrans();
sqlCommitTrans();
sqlRollbackTrans();
Loading