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
16 changes: 15 additions & 1 deletion core.neon
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# PHPStan Extension for OpenEMR Core
#
# This configuration includes rules for OpenEMR core development:
# - Database: Forbid legacy SQL functions and Laminas-DB
# - Database: Forbid legacy SQL functions, methods, and Laminas-DB
# - Http: Forbid raw curl_* functions
# - Globals: Forbid direct $GLOBALS access
# - Testing: Forbid @covers annotations
#
Expand All @@ -23,6 +24,19 @@ services:
tags:
- phpstan.rules.rule

- class: OpenCoreEMR\PHPStan\Rules\Database\ForbiddenMethodsRule
tags:
- phpstan.rules.rule

- class: OpenCoreEMR\PHPStan\Rules\Database\ForbiddenStaticMethodsRule
tags:
- phpstan.rules.rule

# Http Rules
- class: OpenCoreEMR\PHPStan\Rules\Http\ForbiddenCurlFunctionsRule
tags:
- phpstan.rules.rule

# Globals Rules
- class: OpenCoreEMR\PHPStan\Rules\Globals\ForbiddenGlobalsAccessRule
tags:
Expand Down
75 changes: 75 additions & 0 deletions src/Rules/Database/ForbiddenMethodsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/**
* Custom PHPStan Rule to Forbid Deprecated Database Methods
*
* This rule prevents use of deprecated database-related methods.
* Contributors should use QueryUtils or DatabaseQueryTrait instead.
*
* @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\Rules\Database;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<MethodCall>
*/
class ForbiddenMethodsRule implements Rule
{
/**
* Map of forbidden methods to their error messages
*
* (Ideally, these would be scoped to a specific class/interface, but most are
* targeting globals lacking sufficient type info)
*/
private const FORBIDDEN_METHODS = [
'GenID' => 'Use QueryUtils::generateId() or QueryUtils::ediGenerateId() instead.',
];

public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param MethodCall $node
* @return array<\PHPStan\Rules\RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Identifier)) {
return [];
}

$methodName = $node->name->toString();

// Only check if it's a forbidden method
if (!isset(self::FORBIDDEN_METHODS[$methodName])) {
return [];
}

$message = sprintf(
'%s() is deprecated. %s',
$methodName,
self::FORBIDDEN_METHODS[$methodName],
);

return [
RuleErrorBuilder::message($message)
->identifier('openemr.deprecatedMethod')
->tip('Or use DatabaseQueryTrait in your class')
->build()
];
}
}
90 changes: 90 additions & 0 deletions src/Rules/Database/ForbiddenStaticMethodsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* Custom PHPStan Rule to Block Certain Static Method Calls
*
* This rule prevents use of specific static methods that should be avoided
* in favor of safer alternatives.
*
* @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\Rules\Database;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<StaticCall>
*/
class ForbiddenStaticMethodsRule implements Rule
{
/**
* Map of forbidden classes and static methods to their error messages
*/
private const FORBIDDEN_METHODS = [
'OpenEMR\\Common\\Database\\QueryUtils' => [
'startTransaction' => 'Use QueryUtils::inTransaction() wrapper instead.',
'commitTransaction' => 'Use QueryUtils::inTransaction() wrapper instead.',
'rollbackTransaction' => 'Use QueryUtils::inTransaction() wrapper instead.',
],
];

public function getNodeType(): string
{
return StaticCall::class;
}

/**
* @param StaticCall $node
* @return array<\PHPStan\Rules\RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
// Method name must be an Identifier (not a dynamic call)
if (!($node->name instanceof Identifier)) {
return [];
}

// Class must be a Name (not a dynamic class)
if (!($node->class instanceof Name)) {
return [];
}

$className = $node->class->toString();
$methodName = $node->name->toString();

// Check if the class has any forbidden methods
if (!array_key_exists($className, self::FORBIDDEN_METHODS)) {
return [];
}

// If it does, check if the actual call is one of them
$forbiddenClassMethods = self::FORBIDDEN_METHODS[$className];
if (!array_key_exists($methodName, $forbiddenClassMethods)) {
return [];
}

$message = sprintf(
'%s::%s() is deprecated. %s',
$className,
$methodName,
$forbiddenClassMethods[$methodName],
);

return [
RuleErrorBuilder::message($message)
->identifier('openemr.deprecatedStaticMethod')
->build()
];
}
}
69 changes: 69 additions & 0 deletions src/Rules/Http/ForbiddenCurlFunctionsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* Custom PHPStan Rule to Forbid Raw curl_* Functions
*
* This rule prevents use of raw curl_* functions as the project is migrating
* to use PSR-18 HTTP clients for better testability, error handling, and PSR-7 compliance.
*
* @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\Rules\Http;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<FuncCall>
*/
class ForbiddenCurlFunctionsRule implements Rule
{
/**
* Pattern to match all curl_* functions
*/
private const CURL_FUNCTION_PATTERN = '/^curl_/i';

public function getNodeType(): string
{
return FuncCall::class;
}

/**
* @param FuncCall $node
* @return array<\PHPStan\Rules\RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Name)) {
return [];
}

$functionName = $node->name->toString();

// Check if it's a curl_* function
if (!preg_match(self::CURL_FUNCTION_PATTERN, $functionName)) {
return [];
}

return [
RuleErrorBuilder::message(
sprintf(
'Raw curl function %s() is forbidden. Use a PSR-18 HTTP client instead.',
$functionName
)
)
->identifier('openemr.forbiddenCurlFunction')
->tip('See https://www.php-fig.org/psr/psr-18/ for PSR-18 HTTP Client information')
->build()
];
}
}
45 changes: 45 additions & 0 deletions tests/Rules/Database/ForbiddenMethodsRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* Test for ForbiddenMethodsRule
*
* @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\ForbiddenMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

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

public function testForbiddenMethods(): void
{
$tip = 'Or use DatabaseQueryTrait in your class';
$this->analyse([__DIR__ . '/data/forbidden-methods.php'], [
[
'GenID() is deprecated. Use QueryUtils::generateId() or QueryUtils::ediGenerateId() instead.',
14,
$tip,
],
]);
}

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

/**
* Test for ForbiddenStaticMethodsRule
*
* @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\ForbiddenStaticMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

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

public function testForbiddenStaticMethods(): void
{
$this->analyse([__DIR__ . '/data/forbidden-static-methods.php'], [
[
'OpenEMR\Common\Database\QueryUtils::startTransaction() is deprecated. Use QueryUtils::inTransaction() wrapper instead.',
28,
],
[
'OpenEMR\Common\Database\QueryUtils::commitTransaction() is deprecated. Use QueryUtils::inTransaction() wrapper instead.',
29,
],
[
'OpenEMR\Common\Database\QueryUtils::rollbackTransaction() is deprecated. Use QueryUtils::inTransaction() wrapper instead.',
30,
],
]);
}

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

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

class UserService
{
public function generateId(): int
{
return 1;
}

public function getId(): int
{
return 1;
}
}

$service = new UserService();
$id = $service->generateId();
$id = $service->getId();
Loading