Repository: spaze/phpstan-disallowed-calls (php)

Issue:
False Positives on disallowedFunctionCalls Rule in Version 4.1.1
# Description
## Summary:
After upgrading from version `4.0.1` to `4.1.1`, the `disallowedFunctionCalls` rule produces false positives when certain strings are present in method names, incorrectly identifying them as forbidden function calls.

## Steps to Reproduce:

Configure `disallowedFunctionCalls` as follows:
```yaml
parameters:
  disallowedFunctionCalls:
    - function:
        - 'data_get()'
        - 'data_set()'
        - 'optional()'
      message: 'This function is not allowed'
```
Define a method or function with a name that includes the substring of a forbidden function, such as `testItHandlesSomeOptional()`.
Run `PHPStan` analysis.

## Observed Behavior:
PHPStan flags the method as violating the `disallowedFunctionCalls` rule:
```yaml
message: '#^Calling optional\(\) is forbidden, This function is not allowed\.$#'
```

## Expected Behavior:
`PHPStan` should not flag method or function names that merely include substrings matching forbidden function names. Only actual calls to the forbidden functions should trigger a violation.

## Environment:

PHPStan version: 2.1.1
spaze/phpstan-disallowed-calls: 4.1.1
Previous working version: 4.0.1

## Additional Notes:

This appears to be a regression introduced in version `4.1.1`. The pattern matching for the `disallowedFunctionCalls` rule seems to be overly aggressive, treating method or function names as if they were calls to the forbidden functions.

Let me know if more details are needed!




Pull request description:
Check just one callable parameter variant
As suggested in https://github.com/spaze/phpstan-disallowed-calls/issues/288#issuecomment-2609928632

Close #288

Patch:
diff --git a/src/RuleErrors/DisallowedCallableParameterRuleErrors.php b/src/RuleErrors/DisallowedCallableParameterRuleErrors.php
index a3bdf8f..cc2d7bf 100644
--- a/src/RuleErrors/DisallowedCallableParameterRuleErrors.php
+++ b/src/RuleErrors/DisallowedCallableParameterRuleErrors.php
@@ -14,7 +14,6 @@ use PHPStan\Analyser\ArgumentsNormalizer;
 use PHPStan\Analyser\Scope;
 use PHPStan\Reflection\ExtendedMethodReflection;
 use PHPStan\Reflection\FunctionReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
 use PHPStan\Reflection\ReflectionProvider;
 use PHPStan\Rules\IdentifierRuleError;
 use PHPStan\ShouldNotHappenException;
@@ -171,10 +170,13 @@ class DisallowedCallableParameterRuleErrors
 	 */
 	private function getErrors(CallLike $node, Scope $scope, $reflection): array
 	{
+		$variants = $reflection->getVariants();
+		if (count($variants) !== 1) {
+			return [];
+		}
 		$ruleErrors = [];
-		$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $reflection->getVariants());
-		$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptor, $node->getArgs()) ?? $node->getArgs();
-		foreach ($parametersAcceptor->getParameters() as $key => $parameter) {
+		$reorderedArgs = ArgumentsNormalizer::reorderArgs($variants[0], $node->getArgs()) ?? $node->getArgs();
+		foreach ($variants[0]->getParameters() as $key => $parameter) {
 			if (!TypeCombinator::removeNull($parameter->getType())->isCallable()->yes() || !isset($reorderedArgs[$key])) {
 				continue;
 			}


Test patch:
diff --git a/tests/Bugs/Bug288DocblockTemplateCallableParamTest.php b/tests/Bugs/Bug288DocblockTemplateCallableParamTest.php
new file mode 100644
index 0000000..9c2eff8
--- /dev/null
+++ b/tests/Bugs/Bug288DocblockTemplateCallableParamTest.php
@@ -0,0 +1,61 @@
+<?php
+declare(strict_types = 1);
+
+namespace Spaze\PHPStan\Rules\Disallowed\Calls;
+
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Rules\Rule;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Testing\RuleTestCase;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;
+use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver;
+
+class Bug288DocblockTemplateCallableParamTest extends RuleTestCase
+{
+
+	/**
+	 * @throws ShouldNotHappenException
+	 */
+	protected function getRule(): Rule
+	{
+		$container = self::getContainer();
+		$disallowedCallableParameterRuleErrors = new DisallowedCallableParameterRuleErrors(
+			$container->getByType(TypeResolver::class),
+			$container->getByType(DisallowedFunctionRuleErrors::class),
+			$container->getByType(DisallowedMethodRuleErrors::class),
+			$container->getByType(DisallowedCallFactory::class),
+			$container->getByType(ReflectionProvider::class),
+			[
+				[
+					'function' => 'disallowedFunction()',
+				],
+			],
+			[],
+			[],
+		);
+		return new MethodCalls(
+			$container->getByType(DisallowedMethodRuleErrors::class),
+			$disallowedCallableParameterRuleErrors,
+			$container->getByType(DisallowedCallFactory::class),
+			[],
+		);
+	}
+
+
+	public function testRule(): void
+	{
+		$this->analyse([__DIR__ . '/../src/bugs/Bug288DocblockTemplateCallableParam.php'], []);
+	}
+
+
+	public static function getAdditionalConfigFiles(): array
+	{
+		return [
+			__DIR__ . '/../../extension.neon',
+		];
+	}
+
+}
diff --git a/tests/src/bugs/Bug288DocblockTemplateCallableParam.php b/tests/src/bugs/Bug288DocblockTemplateCallableParam.php
new file mode 100644
index 0000000..fa94906
--- /dev/null
+++ b/tests/src/bugs/Bug288DocblockTemplateCallableParam.php
@@ -0,0 +1,28 @@
+<?php
+declare(strict_types = 1);
+
+function disallowedFunction()
+{
+}
+
+class Bug288DocblockTemplateCallableParam
+{
+
+	public function testItDoesNothing(): void
+	{
+		// the $expected param here is a string param, not a callable param, no error should be reported here
+		$this->assertSame('disallowedFunction', 'whatever');
+	}
+
+	/**
+	 * A copy of PHPUnit\Framework\Assert::assertSame()'s docblock
+	 *
+	 * @template ExpectedType
+	 * @param ExpectedType $expected
+	 * @phpstan-assert =ExpectedType $actual
+	 */
+	public function assertSame(mixed $expected, mixed $actual, string $message = ''): void
+	{
+	}
+
+}