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

Issue:
support `tip` 
phpstan [RuleErrorBuilder](https://github.com/phpstan/phpstan-src/blob/1c012538fee1865bf3cd2b756085a462b3840b6a/src/Rules/RuleErrorBuilder.php#L104-L110) supports a `tip` which brings additional information besides the error message.

this tip is rendered below a error message in the analysis result

would be great a `tip` could be configured in the config file, like

```
    disallowedStaticCalls:
        -
            method: 'PotentiallyDangerous\Debugger::log()'
            message: 'use our own logger instead'
            tip: 'see https://our-docs.com/logging on how logging should be used'
```

Pull request description:
Add support for `errorTip` for disallowed calls etc.
Close #125

Patch:
diff --git a/README.md b/README.md
index f74c6ea..a1d3965 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ parameters:
         -
             method: 'PotentiallyDangerous\Logger::log()'  # `function` is an alias of `method`
             message: 'use our own logger instead'
+            errorTip: 'see https://our-docs.example/logging on how logging should be used'
         -
             method: 'Redis::connect()'
             message: 'use our own Redis instead'
@@ -160,6 +161,8 @@ parameters:
             message: 'use DateTimeInterface::ATOM instead'
 ```
 
+The optional `errorTip` key can be used to show an additional message prefixed with 💡 that's rendered below the error message in the analysis result.
+
 The `errorIdentifier` key is optional. It can be used to provide a unique identifier to the PHPStan error.
 
 Use wildcard (`*`) to ignore all functions, methods, classes, namespaces starting with a prefix, for example:
diff --git a/phpstan.neon b/phpstan.neon
index c8b7a2a..205e1f7 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,7 +3,7 @@ parameters:
 		- src
 	level: max
 	typeAliases:
-		ForbiddenCallsConfig: 'array<array{function?:string, method?:string, message?:string, allowIn?:string[], allowInFunctions?:string[], allowInMethods?:string[], allowParamsInAllowed?:array<integer, integer|boolean|string>, allowParamsInAllowedAnyValue?:array<integer, integer>, allowParamsAnywhere?:array<integer, integer|boolean|string>, allowParamsAnywhereAnyValue?:array<integer, integer>, allowExceptParamsInAllowed?:array<integer, integer|boolean|string>, disallowParamsInAllowed?:array<integer, integer|boolean|string>, allowExceptParams?:array<integer, integer|boolean|string>, disallowParams?:array<integer, integer|boolean|string>, allowExceptCaseInsensitiveParams?:array<integer, integer|boolean|string>, disallowCaseInsensitiveParams?:array<integer, integer|boolean|string>, errorIdentifier?:string}>'
+		ForbiddenCallsConfig: 'array<array{function?:string, method?:string, message?:string, allowIn?:string[], allowInFunctions?:string[], allowInMethods?:string[], allowParamsInAllowed?:array<integer, integer|boolean|string>, allowParamsInAllowedAnyValue?:array<integer, integer>, allowParamsAnywhere?:array<integer, integer|boolean|string>, allowParamsAnywhereAnyValue?:array<integer, integer>, allowExceptParamsInAllowed?:array<integer, integer|boolean|string>, disallowParamsInAllowed?:array<integer, integer|boolean|string>, allowExceptParams?:array<integer, integer|boolean|string>, disallowParams?:array<integer, integer|boolean|string>, allowExceptCaseInsensitiveParams?:array<integer, integer|boolean|string>, disallowCaseInsensitiveParams?:array<integer, integer|boolean|string>, errorIdentifier?:string, errorTip?:string}>'
 
 includes:
 	- vendor/phpstan/phpstan/conf/bleedingEdge.neon
diff --git a/src/DisallowedCall.php b/src/DisallowedCall.php
index 9418988..4a6089e 100644
--- a/src/DisallowedCall.php
+++ b/src/DisallowedCall.php
@@ -35,6 +35,9 @@ class DisallowedCall
 	/** @var string|null */
 	private $errorIdentifier;
 
+	/** @var string|null */
+	private $errorTip;
+
 
 	/**
 	 * DisallowedCall constructor.
@@ -48,9 +51,20 @@ class DisallowedCall
 	 * @param array<int, DisallowedCallParam> $allowExceptParamsInAllowed
 	 * @param array<int, DisallowedCallParam> $allowExceptParams
 	 * @param string|null $errorIdentifier
+	 * @param string|null $errorTip
 	 */
-	public function __construct(string $call, ?string $message, array $allowIn, array $allowInCalls, array $allowParamsInAllowed, array $allowParamsAnywhere, array $allowExceptParamsInAllowed, array $allowExceptParams, ?string $errorIdentifier)
-	{
+	public function __construct(
+		string $call,
+		?string $message,
+		array $allowIn,
+		array $allowInCalls,
+		array $allowParamsInAllowed,
+		array $allowParamsAnywhere,
+		array $allowExceptParamsInAllowed,
+		array $allowExceptParams,
+		?string $errorIdentifier,
+		?string $errorTip
+	) {
 		$this->call = $call;
 		$this->message = $message;
 		$this->allowIn = $allowIn;
@@ -60,6 +74,7 @@ class DisallowedCall
 		$this->allowExceptParamsInAllowed = $allowExceptParamsInAllowed;
 		$this->allowExceptParams = $allowExceptParams;
 		$this->errorIdentifier = $errorIdentifier;
+		$this->errorTip = $errorTip;
 	}
 
 
@@ -135,6 +150,12 @@ class DisallowedCall
 	}
 
 
+	public function getErrorTip(): ?string
+	{
+		return $this->errorTip;
+	}
+
+
 	public function getKey(): string
 	{
 		// The key consists of "initial" config values that would be overwritten with more specific details in a custom config.
diff --git a/src/DisallowedCallFactory.php b/src/DisallowedCallFactory.php
index affcb41..77fc100 100644
--- a/src/DisallowedCallFactory.php
+++ b/src/DisallowedCallFactory.php
@@ -63,7 +63,8 @@ class DisallowedCallFactory
 					$allowParamsAnywhere,
 					$allowExceptParamsInAllowed,
 					$allowExceptParams,
-					$disallowed['errorIdentifier'] ?? null
+					$disallowed['errorIdentifier'] ?? null,
+					$disallowed['errorTip'] ?? null
 				);
 				$disallowedCalls[$disallowedCall->getKey()] = $disallowedCall;
 			}
diff --git a/src/DisallowedConstant.php b/src/DisallowedConstant.php
index 2339726..dca0b40 100644
--- a/src/DisallowedConstant.php
+++ b/src/DisallowedConstant.php
@@ -18,6 +18,9 @@ class DisallowedConstant
 	/** @var string|null */
 	private $errorIdentifier;
 
+	/** @var string|null */
+	private $errorTip;
+
 
 	/**
 	 * DisallowedCall constructor.
@@ -26,13 +29,20 @@ class DisallowedConstant
 	 * @param string|null $message
 	 * @param string[] $allowIn
 	 * @param string|null $errorIdentifier
+	 * @param string|null $errorTip
 	 */
-	public function __construct(string $constant, ?string $message, array $allowIn, ?string $errorIdentifier)
-	{
+	public function __construct(
+		string $constant,
+		?string $message,
+		array $allowIn,
+		?string $errorIdentifier,
+		?string $errorTip
+	) {
 		$this->constant = ltrim($constant, '\\');
 		$this->message = $message;
 		$this->allowIn = $allowIn;
 		$this->errorIdentifier = $errorIdentifier;
+		$this->errorTip = $errorTip;
 	}
 
 
@@ -62,4 +72,10 @@ class DisallowedConstant
 		return $this->errorIdentifier;
 	}
 
+
+	public function getErrorTip(): ?string
+	{
+		return $this->errorTip;
+	}
+
 }
diff --git a/src/DisallowedConstantFactory.php b/src/DisallowedConstantFactory.php
index 6e0a7a9..b17a18a 100644
--- a/src/DisallowedConstantFactory.php
+++ b/src/DisallowedConstantFactory.php
@@ -9,7 +9,7 @@ class DisallowedConstantFactory
 {
 
 	/**
-	 * @param array<array{class?:string, constant?:string, message?:string, allowIn?:string[], errorIdentifier?:string}> $config
+	 * @param array<array{class?:string, constant?:string, message?:string, allowIn?:string[], errorIdentifier?:string, errorTip?:string}> $config
 	 * @return DisallowedConstant[]
 	 * @throws ShouldNotHappenException
 	 */
@@ -28,7 +28,8 @@ class DisallowedConstantFactory
 					$class ? "{$class}::{$constant}" : $constant,
 					$disallowed['message'] ?? null,
 					$disallowed['allowIn'] ?? [],
-					$disallowed['errorIdentifier'] ?? null
+					$disallowed['errorIdentifier'] ?? null,
+					$disallowed['errorTip'] ?? null
 				);
 				$disallowedConstants[$disallowedConstant->getConstant()] = $disallowedConstant;
 			}
diff --git a/src/DisallowedHelper.php b/src/DisallowedHelper.php
index 7358589..ac971f3 100644
--- a/src/DisallowedHelper.php
+++ b/src/DisallowedHelper.php
@@ -128,6 +128,9 @@ class DisallowedHelper
 				if ($disallowedCall->getErrorIdentifier()) {
 					$errorBuilder->identifier($disallowedCall->getErrorIdentifier());
 				}
+				if ($disallowedCall->getErrorTip()) {
+					$errorBuilder->tip($disallowedCall->getErrorTip());
+				}
 				return [
 					$errorBuilder->build(),
 				];
@@ -236,6 +239,9 @@ class DisallowedHelper
 				if ($disallowedConstant->getErrorIdentifier()) {
 					$errorBuilder->identifier($disallowedConstant->getErrorIdentifier());
 				}
+				if ($disallowedConstant->getErrorTip()) {
+					$errorBuilder->tip($disallowedConstant->getErrorTip());
+				}
 				return [
 					$errorBuilder->build(),
 				];
diff --git a/src/DisallowedNamespace.php b/src/DisallowedNamespace.php
index 258dfe5..c458714 100644
--- a/src/DisallowedNamespace.php
+++ b/src/DisallowedNamespace.php
@@ -18,19 +18,29 @@ class DisallowedNamespace
 	/** @var string|null */
 	private $errorIdentifier;
 
+	/** @var string|null */
+	private $errorTip;
+
 
 	/**
 	 * @param string $namespace
 	 * @param string|null $message
 	 * @param string[] $allowIn
 	 * @param string|null $errorIdentifier
+	 * @param string|null $errorTip
 	 */
-	public function __construct(string $namespace, ?string $message, array $allowIn, ?string $errorIdentifier)
-	{
+	public function __construct(
+		string $namespace,
+		?string $message,
+		array $allowIn,
+		?string $errorIdentifier,
+		?string $errorTip
+	) {
 		$this->namespace = ltrim($namespace, '\\');
 		$this->message = $message;
 		$this->allowIn = $allowIn;
 		$this->errorIdentifier = $errorIdentifier;
+		$this->errorTip = $errorTip;
 	}
 
 
@@ -60,4 +70,10 @@ class DisallowedNamespace
 		return $this->errorIdentifier;
 	}
 
+
+	public function getErrorTip(): ?string
+	{
+		return $this->errorTip;
+	}
+
 }
diff --git a/src/DisallowedNamespaceFactory.php b/src/DisallowedNamespaceFactory.php
index 907abe3..2b1bfd4 100644
--- a/src/DisallowedNamespaceFactory.php
+++ b/src/DisallowedNamespaceFactory.php
@@ -9,7 +9,7 @@ class DisallowedNamespaceFactory
 {
 
 	/**
-	 * @param array<array{namespace?:string, class?:string, message?:string, allowIn?:string[], errorIdentifier?:string}> $config
+	 * @param array<array{namespace?:string, class?:string, message?:string, allowIn?:string[], errorIdentifier?:string, errorTip?:string}> $config
 	 * @return DisallowedNamespace[]
 	 */
 	public function createFromConfig(array $config): array
@@ -26,7 +26,8 @@ class DisallowedNamespaceFactory
 					$namespace,
 					$disallowed['message'] ?? null,
 					$disallowed['allowIn'] ?? [],
-					$disallowed['errorIdentifier'] ?? null
+					$disallowed['errorIdentifier'] ?? null,
+					$disallowed['errorTip'] ?? null
 				);
 				$disallowedNamespaces[$disallowedNamespace->getNamespace()] = $disallowedNamespace;
 			}
diff --git a/src/DisallowedNamespaceHelper.php b/src/DisallowedNamespaceHelper.php
index 779e091..14eb50e 100644
--- a/src/DisallowedNamespaceHelper.php
+++ b/src/DisallowedNamespaceHelper.php
@@ -64,6 +64,9 @@ class DisallowedNamespaceHelper
 			if ($disallowedNamespace->getErrorIdentifier()) {
 				$errorBuilder->identifier($disallowedNamespace->getErrorIdentifier());
 			}
+			if ($disallowedNamespace->getErrorTip()) {
+				$errorBuilder->tip($disallowedNamespace->getErrorTip());
+			}
 			return [
 				$errorBuilder->build(),
 			];
diff --git a/src/DisallowedSuperglobalFactory.php b/src/DisallowedSuperglobalFactory.php
index 8263769..038d5eb 100644
--- a/src/DisallowedSuperglobalFactory.php
+++ b/src/DisallowedSuperglobalFactory.php
@@ -25,7 +25,7 @@ class DisallowedSuperglobalFactory implements DisallowedVariableFactory
 
 
 	/**
-	 * @param array<array{superglobal?:string, message?:string, allowIn?:string[], errorIdentifier?:string}> $config
+	 * @param array<array{superglobal?:string, message?:string, allowIn?:string[], errorIdentifier?:string, errorTip?:string}> $config
 	 * @return DisallowedVariable[]
 	 * @throws ShouldNotHappenException
 	 */
@@ -46,7 +46,8 @@ class DisallowedSuperglobalFactory implements DisallowedVariableFactory
 					$superglobal,
 					$disallowed['message'] ?? null,
 					$disallowed['allowIn'] ?? [],
-					$disallowed['errorIdentifier'] ?? null
+					$disallowed['errorIdentifier'] ?? null,
+					$disallowed['errorTip'] ?? null
 				);
 				$disallowedSuperglobals[$disallowedSuperglobal->getVariable()] = $disallowedSuperglobal;
 			}
diff --git a/src/DisallowedVariable.php b/src/DisallowedVariable.php
index 198d0b4..1d6c908 100644
--- a/src/DisallowedVariable.php
+++ b/src/DisallowedVariable.php
@@ -18,19 +18,29 @@ class DisallowedVariable
 	/** @var string|null */
 	private $errorIdentifier;
 
+	/** @var string|null */
+	private $errorTip;
+
 
 	/**
 	 * @param string $variable
 	 * @param string|null $message
 	 * @param string[] $allowIn
 	 * @param string|null $errorIdentifier
+	 * @param string|null $errorTip
 	 */
-	public function __construct(string $variable, ?string $message, array $allowIn, ?string $errorIdentifier)
-	{
+	public function __construct(
+		string $variable,
+		?string $message,
+		array $allowIn,
+		?string $errorIdentifier,
+		?string $errorTip
+	) {
 		$this->variable = $variable;
 		$this->message = $message;
 		$this->allowIn = $allowIn;
 		$this->errorIdentifier = $errorIdentifier;
+		$this->errorTip = $errorTip;
 	}
 
 
@@ -60,4 +70,10 @@ class DisallowedVariable
 		return $this->errorIdentifier;
 	}
 
+
+	public function getErrorTip(): ?string
+	{
+		return $this->errorTip;
+	}
+
 }
diff --git a/src/DisallowedVariableHelper.php b/src/DisallowedVariableHelper.php
index 687d56a..f07f246 100644
--- a/src/DisallowedVariableHelper.php
+++ b/src/DisallowedVariableHelper.php
@@ -49,6 +49,9 @@ class DisallowedVariableHelper
 				if ($disallowedVariable->getErrorIdentifier()) {
 					$errorBuilder->identifier($disallowedVariable->getErrorIdentifier());
 				}
+				if ($disallowedVariable->getErrorTip()) {
+					$errorBuilder->tip($disallowedVariable->getErrorTip());
+				}
 				return [
 					$errorBuilder->build(),
 				];


Test patch:
diff --git a/tests/Calls/FunctionCallsTest.php b/tests/Calls/FunctionCallsTest.php
index 0c0de03..0f0b016 100644
--- a/tests/Calls/FunctionCallsTest.php
+++ b/tests/Calls/FunctionCallsTest.php
@@ -26,6 +26,7 @@ class FunctionCallsTest extends RuleTestCase
 						'../src/disallowed-allowed/*.php',
 						'../src/*-allow/*.*',
 					],
+					'errorTip' => 'See docs',
 				],
 				[
 					'function' => 'print_r()',
@@ -159,6 +160,7 @@ class FunctionCallsTest extends RuleTestCase
 				'Calling var_dump() is forbidden, use logger instead',
 				// on this line:
 				7,
+				'See docs',
 			],
 			[
 				'Calling print_R() is forbidden, nope [print_R() matches print_r()]',
diff --git a/tests/Calls/MethodCallsTest.php b/tests/Calls/MethodCallsTest.php
index 1bcbbb4..edf86a0 100644
--- a/tests/Calls/MethodCallsTest.php
+++ b/tests/Calls/MethodCallsTest.php
@@ -107,6 +107,7 @@ class MethodCallsTest extends RuleTestCase
 					'allowExceptParams' => [
 						1 => 'y',
 					],
+					'errorTip' => '2038 is approaching fast',
 				],
 			]
 		);
@@ -158,6 +159,7 @@ class MethodCallsTest extends RuleTestCase
 			[
 				'Calling DateTime::format() is forbidden, why too kay',
 				55,
+				'2038 is approaching fast',
 			],
 			[
 				'Calling Waldo\Quux\Blade::movie() is forbidden, was good',
diff --git a/tests/Usages/ConstantUsagesTest.php b/tests/Usages/ConstantUsagesTest.php
index 78d1e10..86ab3c0 100644
--- a/tests/Usages/ConstantUsagesTest.php
+++ b/tests/Usages/ConstantUsagesTest.php
@@ -37,6 +37,7 @@ class ConstantUsagesTest extends RuleTestCase
 						'../src/disallowed-allowed/*.php',
 						'../src/*-allow/*.*',
 					],
+					'errorTip' => 'Use https://github.com/mlocati/ip-lib instead',
 				],
 			]
 		);
@@ -52,10 +53,12 @@ class ConstantUsagesTest extends RuleTestCase
 				'Using FILTER_FLAG_NO_PRIV_RANGE is forbidden, the cake is a lie',
 				// on this line:
 				8,
+				'Use https://github.com/mlocati/ip-lib instead',
 			],
 			[
 				'Using FILTER_FLAG_NO_PRIV_RANGE is forbidden, the cake is a lie',
 				9,
+				'Use https://github.com/mlocati/ip-lib instead',
 			],
 			[
 				'Using FILTER_FLAG_NO_RES_RANGE is forbidden, the cake is a lie',
diff --git a/tests/Usages/NamespaceUsagesTest.php b/tests/Usages/NamespaceUsagesTest.php
index 3c9f81a..ebf2cc6 100644
--- a/tests/Usages/NamespaceUsagesTest.php
+++ b/tests/Usages/NamespaceUsagesTest.php
@@ -26,6 +26,7 @@ class NamespaceUsagesTest extends RuleTestCase
 						'../src/disallowed-allowed/*.php',
 						'../src/*-allow/*.*',
 					],
+					'errorTip' => 'Work more on your frames',
 				],
 				[
 					'namespace' => [
@@ -76,6 +77,7 @@ class NamespaceUsagesTest extends RuleTestCase
 				'Namespace Framework\SomeInterface is forbidden, no framework some [Framework\SomeInterface matches Framew*rk\Some*]',
 				// on this line:
 				6,
+				'Work more on your frames',
 			],
 			[
 				'Namespace Inheritance\Base is forbidden, no inheritance sub base',
@@ -104,6 +106,7 @@ class NamespaceUsagesTest extends RuleTestCase
 			[
 				'Namespace Framework\SomeInterface is forbidden, no framework some [Framework\SomeInterface matches Framew*rk\Some*]',
 				13,
+				'Work more on your frames',
 			],
 			[
 				'Trait Traits\TestTrait is forbidden, no TestTrait',
diff --git a/tests/Usages/SuperglobalUsagesTest.php b/tests/Usages/SuperglobalUsagesTest.php
index 548c86e..52e6855 100644
--- a/tests/Usages/SuperglobalUsagesTest.php
+++ b/tests/Usages/SuperglobalUsagesTest.php
@@ -25,6 +25,7 @@ class SuperglobalUsagesTest extends RuleTestCase
 						'../src/disallowed-allowed/*.php',
 						'../src/*-allow/*.*',
 					],
+					'errorTip' => 'So long and thanks for all the tips',
 				],
 				[
 					'superglobal' => [
@@ -51,6 +52,7 @@ class SuperglobalUsagesTest extends RuleTestCase
 				'Using $GLOBALS is forbidden, the cake is a lie',
 				// on this line:
 				8,
+				'So long and thanks for all the tips',
 			],
 			[
 				'Using $_GET is forbidden, the cake is a lie',