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:
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
+	{
+	}
+
+}