Skip to content

Commit

Permalink
Can disallow control structures like else, elseif, goto (#257)
Browse files Browse the repository at this point in the history
Checking params inside ( ... ) doesn't work at the moment, so you can disallow all `declare()`s but can't re-allow e.g. `declare(strict-types = 1)`.

If you try to disallow `else if` with the space, an exception will be thrown, because `else if` is parsed as `else` followed by `if`, so disallowing `else if` with the space wouldn't have the desired effect and the result would be unexpected.

Close #68
  • Loading branch information
spaze committed May 4, 2024
2 parents 6d5ce7e + 5b5bef6 commit d58806c
Show file tree
Hide file tree
Showing 38 changed files with 2,602 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
},
"scripts": {
"lint": "vendor/bin/parallel-lint --colors src/ tests/",
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php",
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php",
"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php",
"lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
"lint-8.2": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
Expand Down
27 changes: 27 additions & 0 deletions docs/custom-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ There are several different types (and configuration keys) that can be disallowe
6. `disallowedSuperglobals` - for usages of superglobal variables like `$GLOBALS` or `$_POST`
7. `disallowedAttributes` - for attributes like `#[Entity(class: Foo::class, something: true)]`
8. `disallowedEnums` - for enums, both pure & backed, like `Suit::Hearts` (like class constants, enums need to be split to `enum: Suit` & `case: Hearts` in the configuration, see notes below)
9. `disallowedControlStructures` - for control structures like `if`, `else`, `elseif`, loops, `require` & `include`, and `goto`

Use them to add rules to your `phpstan.neon` config file. I like to use a separate file (`disallowed-calls.neon`) for these which I'll include later on in the main `phpstan.neon` config file. Here's an example, update to your needs:

Expand Down Expand Up @@ -165,10 +166,36 @@ You can treat some language constructs as functions and disallow it in `disallow

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.

You can also disallow control structures, see below.

### Constants

When [disallowing constants](disallowing-constants.md) please be aware of limitations and special requirements, see [docs](disallowing-constants.md).

### Enums

Similar to disallowing constants, enums have some limitations, see [docs](disallowing-enums.md).

### Control structures

You can forbid [control structures](https://www.php.net/language.control-structures) like `if`, `else`, `elseif` (don't write it as `else if` because `else if` is parsed as `else` followed by `if` producing unexpected results), loops, `break`, `continue`, `goto`, `require`, `include` (and the `_once` variants).

Currently, parameters are not checked, so it's not possible to disallow for example `declare`, but re-allow `declare(strict-types = 1)`.

You can use `controlStructure` or `structure` directives, and both can be specified as arrays:
```neon
parameters:
disallowedControlStructures:
-
controlStructure:
- 'elseif'
- 'return'
allowIn:
- 'tests/foo/bar.php'
-
structure: 'goto'
message: "restructure the program's flow"
errorTip: 'https://xkcd.com/292/'
allowIn:
- 'tests/waldo.php'
```
75 changes: 75 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ parameters:
disallowedEnums: []
disallowedSuperglobals: []
disallowedAttributes: []
disallowedControlStructures: []

parametersSchema:
allowInRootDir: schema(string(), nullable())
Expand Down Expand Up @@ -224,13 +225,26 @@ parametersSchema:
?errorTip: string(),
])
)
disallowedControlStructures: listOf(
structure([
?controlStructure: anyOf(string(), listOf(string())),
?structure: anyOf(string(), listOf(string())),
?message: string(),
?allowIn: listOf(string()),
?allowExceptIn: list(string()),
?disallowIn: list(string()),
?errorIdentifier: string(),
?errorTip: string(),
])
)

services:
- Spaze\PHPStan\Rules\Disallowed\Allowed\Allowed
- Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedPath
- Spaze\PHPStan\Rules\Disallowed\DisallowedAttributeFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedConstantFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedNamespaceFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedSuperglobalFactory
- Spaze\PHPStan\Rules\Disallowed\File\FilePath(rootDir: %filesRootDir%)
Expand All @@ -239,6 +253,7 @@ services:
- Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedNamespaceRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors
Expand Down Expand Up @@ -312,3 +327,63 @@ services:
factory: Spaze\PHPStan\Rules\Disallowed\Usages\AttributeUsages(disallowedAttributes: %disallowedAttributes%)
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\BreakControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ContinueControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\DeclareControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\DoWhileControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ElseControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ElseIfControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ForControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ForeachControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\GotoControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\IfControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\MatchControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\RequireIncludeControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\ReturnControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\SwitchControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\WhileControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
59 changes: 59 additions & 0 deletions src/ControlStructures/BreakControlStructure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed\ControlStructures;

use PhpParser\Node;
use PhpParser\Node\Stmt\Break_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructure;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors;

/**
* Reports on using the break control structure.
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<Break_>
*/
class BreakControlStructure implements Rule
{

/** @var DisallowedControlStructureRuleErrors */
private $disallowedControlStructureRuleErrors;

/** @var list<DisallowedControlStructure> */
private $disallowedControlStructures;


/**
* @param DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors
* @param list<DisallowedControlStructure> $disallowedControlStructures
*/
public function __construct(DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors, array $disallowedControlStructures)
{
$this->disallowedControlStructureRuleErrors = $disallowedControlStructureRuleErrors;
$this->disallowedControlStructures = $disallowedControlStructures;
}


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


/**
* @param Break_ $node
* @param Scope $scope
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedControlStructureRuleErrors->get($scope, 'break', $this->disallowedControlStructures);
}

}
59 changes: 59 additions & 0 deletions src/ControlStructures/ContinueControlStructure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed\ControlStructures;

use PhpParser\Node;
use PhpParser\Node\Stmt\Continue_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructure;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors;

/**
* Reports on using the continue control structure.
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<Continue_>
*/
class ContinueControlStructure implements Rule
{

/** @var DisallowedControlStructureRuleErrors */
private $disallowedControlStructureRuleErrors;

/** @var list<DisallowedControlStructure> */
private $disallowedControlStructures;


/**
* @param DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors
* @param list<DisallowedControlStructure> $disallowedControlStructures
*/
public function __construct(DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors, array $disallowedControlStructures)
{
$this->disallowedControlStructureRuleErrors = $disallowedControlStructureRuleErrors;
$this->disallowedControlStructures = $disallowedControlStructures;
}


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


/**
* @param Continue_ $node
* @param Scope $scope
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedControlStructureRuleErrors->get($scope, 'continue', $this->disallowedControlStructures);
}

}
59 changes: 59 additions & 0 deletions src/ControlStructures/DeclareControlStructure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed\ControlStructures;

use PhpParser\Node;
use PhpParser\Node\Stmt\Declare_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructure;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors;

/**
* Reports on using the declare control structure.
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<Declare_>
*/
class DeclareControlStructure implements Rule
{

/** @var DisallowedControlStructureRuleErrors */
private $disallowedControlStructureRuleErrors;

/** @var list<DisallowedControlStructure> */
private $disallowedControlStructures;


/**
* @param DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors
* @param list<DisallowedControlStructure> $disallowedControlStructures
*/
public function __construct(DisallowedControlStructureRuleErrors $disallowedControlStructureRuleErrors, array $disallowedControlStructures)
{
$this->disallowedControlStructureRuleErrors = $disallowedControlStructureRuleErrors;
$this->disallowedControlStructures = $disallowedControlStructures;
}


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


/**
* @param Declare_ $node
* @param Scope $scope
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedControlStructureRuleErrors->get($scope, 'declare', $this->disallowedControlStructures);
}

}
Loading

0 comments on commit d58806c

Please sign in to comment.