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

Issue:
Disallowing `isset` calls
Currently, [certain language constructs can be disallowed](https://github.com/spaze/phpstan-disallowed-calls/blob/27849e500f301a83e720c94c83de738a11a65040/docs/custom-rules.md#language-constructs-and-constructors), though `isset` is not among them. 

I'm happy to implement support for `isset` if it aligns with the library's goals, just let me know.

Pull request:
feat: support isset() language construct
Add support for `isset()` language construct.

We would like to use `phpstan-disallowed-calls` and disallow `isset()` in the code. Currently the `isset()` language construct is not supported. 

Context:
* https://dev.to/aleksikauppila/using-isset-and-empty-hurts-your-code-aaa
* https://medium.com/@liamhammett/a-look-at-phps-isset-df64df7158ab

Close #290

Patch:
diff --git a/docs/custom-rules.md b/docs/custom-rules.md
index 6735d6e..34bf504 100644
--- a/docs/custom-rules.md
+++ b/docs/custom-rules.md
@@ -163,6 +163,7 @@ You can treat some language constructs as functions and disallow it in `disallow
 - `empty()`
 - `eval()`
 - `exit()`
+- `isset()`
 - `print()`
 
 To disallow naive object creation (`new ClassName()` or `new $classname`), disallow `NameSpace\ClassName::__construct` in `disallowedMethodCalls`. Works even when there's no constructor defined in that class.
diff --git a/extension.neon b/extension.neon
index 880a9d2..170a22f 100644
--- a/extension.neon
+++ b/extension.neon
@@ -297,6 +297,10 @@ services:
 		factory: Spaze\PHPStan\Rules\Disallowed\Calls\EmptyCalls(forbiddenCalls: %disallowedFunctionCalls%)
 		tags:
 			- phpstan.rules.rule
+	-
+		factory: Spaze\PHPStan\Rules\Disallowed\Calls\IssetCalls(forbiddenCalls: %disallowedFunctionCalls%)
+		tags:
+			- phpstan.rules.rule
 	-
 		factory: Spaze\PHPStan\Rules\Disallowed\Calls\ExitDieCalls(forbiddenCalls: %disallowedFunctionCalls%)
 		tags:
diff --git a/src/Calls/IssetCalls.php b/src/Calls/IssetCalls.php
new file mode 100644
index 0000000..ce78684
--- /dev/null
+++ b/src/Calls/IssetCalls.php
@@ -0,0 +1,60 @@
+<?php
+declare(strict_types = 1);
+
+namespace Spaze\PHPStan\Rules\Disallowed\Calls;
+
+use PhpParser\Node;
+use PhpParser\Node\Expr\Isset_;
+use PHPStan\Analyser\Scope;
+use PHPStan\Rules\Rule;
+use PHPStan\ShouldNotHappenException;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\ErrorIdentifiers;
+
+/**
+ * Reports on dynamically calling isset().
+ *
+ * @package Spaze\PHPStan\Rules\Disallowed
+ * @implements Rule<Isset_>
+ */
+class IssetCalls implements Rule
+{
+
+	private DisallowedCallsRuleErrors $disallowedCallsRuleErrors;
+
+	/** @var list<DisallowedCall> */
+	private array $disallowedCalls;
+
+
+	/**
+	 * @param DisallowedCallsRuleErrors $disallowedCallsRuleErrors
+	 * @param DisallowedCallFactory $disallowedCallFactory
+	 * @param array $forbiddenCalls
+	 * @phpstan-param ForbiddenCallsConfig $forbiddenCalls
+	 * @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
+	 * @throws ShouldNotHappenException
+	 */
+	public function __construct(DisallowedCallsRuleErrors $disallowedCallsRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
+	{
+		$this->disallowedCallsRuleErrors = $disallowedCallsRuleErrors;
+		$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
+	}
+
+
+	public function getNodeType(): string
+	{
+		return Isset_::class;
+	}
+
+
+	/**
+	 * @throws ShouldNotHappenException
+	 */
+	public function processNode(Node $node, Scope $scope): array
+	{
+		return $this->disallowedCallsRuleErrors->get(null, $scope, 'isset', 'isset', null, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_ISSET);
+	}
+
+}
diff --git a/src/RuleErrors/ErrorIdentifiers.php b/src/RuleErrors/ErrorIdentifiers.php
index 52a15e6..6724ad9 100644
--- a/src/RuleErrors/ErrorIdentifiers.php
+++ b/src/RuleErrors/ErrorIdentifiers.php
@@ -30,6 +30,7 @@ class ErrorIdentifiers
 	public const DISALLOWED_IF = 'disallowed.if';
 	public const DISALLOWED_INCLUDE = 'disallowed.include';
 	public const DISALLOWED_INCLUDE_ONCE = 'disallowed.includeOnce';
+	public const DISALLOWED_ISSET = 'disallowed.isset';
 	public const DISALLOWED_MATCH = 'disallowed.match';
 	public const DISALLOWED_METHOD = 'disallowed.method';
 	public const DISALLOWED_NAMESPACE = 'disallowed.namespace';


Test patch:
diff --git a/tests/Calls/IssetCallsTest.php b/tests/Calls/IssetCallsTest.php
new file mode 100644
index 0000000..64c55c5
--- /dev/null
+++ b/tests/Calls/IssetCallsTest.php
@@ -0,0 +1,58 @@
+<?php
+declare(strict_types = 1);
+
+namespace Spaze\PHPStan\Rules\Disallowed\Calls;
+
+use PHPStan\Rules\Rule;
+use PHPStan\ShouldNotHappenException;
+use PHPStan\Testing\RuleTestCase;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
+use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
+
+class IssetCallsTest extends RuleTestCase
+{
+
+	/**
+	 * @throws ShouldNotHappenException
+	 */
+	protected function getRule(): Rule
+	{
+		$container = self::getContainer();
+		return new IssetCalls(
+			$container->getByType(DisallowedCallsRuleErrors::class),
+			$container->getByType(DisallowedCallFactory::class),
+			[
+				[
+					'function' => 'isset()',
+					'allowIn' => [
+						__DIR__ . '/../src/disallowed-allow/*.php',
+						__DIR__ . '/../src/*-allow/*.*',
+					],
+				],
+			]
+		);
+	}
+
+
+	public function testRule(): void
+	{
+		// Based on the configuration above, in this file:
+		$this->analyse([__DIR__ . '/../src/disallowed/functionCalls.php'], [
+			[
+				'Calling isset() is forbidden.',
+				120,
+			],
+		]);
+		// Based on the configuration above, no errors in this file:
+		$this->analyse([__DIR__ . '/../src/disallowed-allow/functionCalls.php'], []);
+	}
+
+
+	public static function getAdditionalConfigFiles(): array
+	{
+		return [
+			__DIR__ . '/../../extension.neon',
+		];
+	}
+
+}
diff --git a/tests/src/disallowed-allow/functionCalls.php b/tests/src/disallowed-allow/functionCalls.php
index 293e72a..f6f8947 100644
--- a/tests/src/disallowed-allow/functionCalls.php
+++ b/tests/src/disallowed-allow/functionCalls.php
@@ -116,3 +116,5 @@ ${'sneaky'}('foo');
 
 ('Foo\Bar\waldo')('foo');
 ('\Foo\Bar\Waldo')('foo');
+
+isset($bottle);
diff --git a/tests/src/disallowed/functionCalls.php b/tests/src/disallowed/functionCalls.php
index 048c2a6..1771787 100644
--- a/tests/src/disallowed/functionCalls.php
+++ b/tests/src/disallowed/functionCalls.php
@@ -116,3 +116,5 @@ ${'sneaky'}('foo');
 
 ('Foo\Bar\waldo')('foo');
 ('\Foo\Bar\Waldo')('foo');
+
+isset($bottle);