Skip to content

Commit 6056c80

Browse files
authored
First rule (#4)
1 parent 517803e commit 6056c80

File tree

13 files changed

+352
-7
lines changed

13 files changed

+352
-7
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
/.idea/
2+
/.php-cs-fixer.cache
3+
/.phpunit.cache
14
/vendor/
2-
.idea/

.php-cs-fixer.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
use PhpCsFixer\Config;
4+
use PhpCsFixer\Finder;
5+
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
6+
7+
$finder = (new Finder())
8+
->in([
9+
__DIR__.'/src',
10+
__DIR__.'/tests',
11+
]);
12+
13+
return (new Config())
14+
->setParallelConfig(ParallelConfigFactory::detect())
15+
->setRules([
16+
'declare_strict_types' => true,
17+
'new_with_braces' => true,
18+
'ordered_class_elements' => true,
19+
'ordered_imports' => true,
20+
'phpdoc_align' => true,
21+
'phpdoc_indent' => true,
22+
'phpdoc_to_comment' => false,
23+
'align_multiline_comment' => ['comment_type' => 'phpdocs_only'],
24+
'concat_space' => ['spacing' => 'one'],
25+
'return_type_declaration' => true,
26+
'method_argument_space' => [
27+
'on_multiline' => 'ensure_fully_multiline',
28+
],
29+
'types_spaces' => [
30+
'space' => 'single',
31+
],
32+
])
33+
->setRiskyAllowed(true)
34+
->setFinder($finder);

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
1-
# phpstan-rules
1+
# nijidigital/phpstan-rules
2+
3+
Custom PhpStan rules made with ❤️ by [Niji](https://www.niji.fr).
4+
5+
## Installation
6+
7+
To use this extension, require it in [Composer](https://getcomposer.org/):
8+
9+
```shell
10+
composer require --dev nijidigital/phpstan-rules
11+
```
12+
13+
If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set!
14+
15+
<details>
16+
<summary>Manual installation</summary>
17+
18+
If you don't want to use `phpstan/extension-installer`, include phpstan.neon in your project's PHPStan config:
19+
20+
```
21+
includes:
22+
- vendor/nijidigital/phpstan-rules/phpstan.neon
23+
```
24+
</details>
25+
26+
## Troubleshooting
27+
28+
### PhpStorm doesn't provide any autocomplete on PhpStan classes
29+
30+
<details>
31+
Taken from [this blog](https://blog.bitexpert.de/blog/phpstorm_phpstan_wsl2_issue) :
32+
33+
1. Copy the file `vendor/phpstan/phpstan/phpstan.phar` to a local folder of your choice, ie : `C:\php_include_path`.
34+
2. In PhpStorm, go to File | Settings | PHP and add your newly created folder to the include path.
35+
36+
</details>

Taskfile.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ tasks:
5353
cmds:
5454
- task run
5555
- task composer_install
56-
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/rector process src tests
57-
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/phpcs src tests --standard=PSR12
58-
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/phpstan analyse src tests --level max
56+
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/rector process --config rector.php
57+
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/php-cs-fixer fix --config .php-cs-fixer.php --diff
58+
- docker exec -u admin -it {{.CONTAINER_NAME}} vendor/bin/phpstan analyse --level max --configuration phpstan.neon src tests
5959

6060
default:
6161
desc: List available tasks
6262
cmds:
63-
- task --list
63+
- task --list

composer.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
"NijiDigital\\PhpStanRules\\": "src/"
88
}
99
},
10+
"autoload-dev": {
11+
"psr-4": {
12+
"NijiDigital\\PhpStanRules\\Tests\\": "tests/"
13+
}
14+
},
1015
"authors": [
1116
{
1217
"name": "dsf-niji"
@@ -21,5 +26,12 @@
2126
"phpunit/phpunit": "^11.5",
2227
"rector/rector": "^1.0",
2328
"friendsofphp/php-cs-fixer": "^3.86"
29+
},
30+
"extra": {
31+
"phpstan": {
32+
"includes": [
33+
"phpstan.neon"
34+
]
35+
}
2436
}
2537
}

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpstan.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
rules:
2+
- NijiDigital\PhpStanRules\Rules\NoRelativeDatetime
3+
4+
parameters:
5+
excludePaths:
6+
- tests/*/data/*

phpunit.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="tests/bootstrap.php"
5+
cacheDirectory=".phpunit.cache"
6+
executionOrder="depends,defects"
7+
shortenArraysForExportThreshold="10"
8+
beStrictAboutOutputDuringTests="true"
9+
displayDetailsOnPhpunitDeprecations="true"
10+
failOnPhpunitDeprecation="true"
11+
failOnRisky="true"
12+
failOnWarning="true">
13+
<testsuites>
14+
<testsuite name="default">
15+
<directory>tests</directory>
16+
</testsuite>
17+
</testsuites>
18+
19+
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
20+
<include>
21+
<directory>src</directory>
22+
</include>
23+
</source>
24+
</phpunit>

rector.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
7+
return RectorConfig::configure()
8+
->withPaths([
9+
__DIR__ . '/src',
10+
__DIR__ . '/tests',
11+
])
12+
->withPhpSets()
13+
->withImportNames(importShortClasses: false, removeUnusedImports: true)
14+
->withAttributesSets(phpunit: true)
15+
->withPreparedSets(
16+
deadCode: true,
17+
codeQuality: true,
18+
codingStyle: true,
19+
typeDeclarations: true,
20+
privatization: true,
21+
naming: true,
22+
instanceOf: true,
23+
earlyReturn: true,
24+
strictBooleans: true,
25+
phpunitCodeQuality: true,
26+
phpunit: true
27+
);

src/Rules/NoRelativeDatetime.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NijiDigital\PhpStanRules\Rules;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\New_;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Scalar\String_;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleError;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use Psr\Clock\ClockInterface;
16+
17+
/**
18+
* @implements Rule<New_>
19+
*/
20+
class NoRelativeDatetime implements Rule
21+
{
22+
public const TIP = 'Use dependency injection to get an instance of ' . ClockInterface::class;
23+
24+
#[\Override]
25+
public function getNodeType(): string
26+
{
27+
return New_::class;
28+
}
29+
30+
/**
31+
* @return RuleError[]
32+
*/
33+
#[\Override]
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!$node instanceof New_) {
37+
return [];
38+
}
39+
40+
if (!$node->class instanceof Name) {
41+
return [];
42+
}
43+
44+
$className = $node->class->toString();
45+
if (!in_array($className, [\DateTime::class, \DateTimeImmutable::class], true)) {
46+
return [];
47+
}
48+
49+
if ([] === $node->getArgs()) {
50+
return [
51+
RuleErrorBuilder::message(
52+
sprintf(
53+
'Usage of %s constructor without any argument is forbidden. Use %s::now().',
54+
$className,
55+
ClockInterface::class
56+
)
57+
)
58+
->identifier('noRelativeDatetime.emptyConstructorCall')
59+
->tip(self::TIP)
60+
->build(),
61+
];
62+
}
63+
64+
$firstArg = $node->getArgs()[0]->value;
65+
if (!$firstArg instanceof String_) {
66+
return [];
67+
}
68+
69+
$dateString = strtolower($firstArg->value);
70+
71+
$relativeKeywords = [
72+
'now', 'today', 'tomorrow', 'yesterday',
73+
'next', 'last', 'first', 'second',
74+
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
75+
'january', 'february', 'march', 'april', 'may', 'june', 'july',
76+
'august', 'september', 'october', 'november', 'december',
77+
'week', 'month', 'year', 'day',
78+
'hour', 'minute', 'second',
79+
];
80+
81+
foreach ($relativeKeywords as $relativeKeyword) {
82+
if (str_contains($dateString, $relativeKeyword)) {
83+
return [
84+
RuleErrorBuilder::message(
85+
sprintf(
86+
'Usage of relative date format ("%1$s") in %2$s is forbidden. Use %3$s::now()->modify("%1$s").',
87+
$dateString,
88+
$className,
89+
ClockInterface::class
90+
)
91+
)
92+
->identifier('noRelativeDatetime.relativeDate')
93+
->tip(self::TIP)
94+
->build(),
95+
];
96+
}
97+
}
98+
99+
return [];
100+
}
101+
}

0 commit comments

Comments
 (0)