Skip to content

Commit b030b0e

Browse files
committed
refactor(rectors): Rename HasOptionsDocCommentRector to HasOptionsRector and improve options
- Rename class/file and update registries to use HasOptionsRector - Add options doc generation and list-type method/property stubs for array options - Add imports (Collection, Pluralizer, PropertyBuilder, SimplePhpParser) and update type mapping to treat '[]' suffix as array Signed-off-by: guanguans <[email protected]>
1 parent cc76977 commit b030b0e

File tree

8 files changed

+162
-40
lines changed

8 files changed

+162
-40
lines changed

baselines/loader.neon

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
# total 15 errors
1+
# total 16 errors
22

33
includes:
44
- assign.propertyType.neon
55
- disallowed.function.neon
66
- function.alreadyNarrowedType.neon
7+
- method.nonObject.neon
78
- new.static.neon
89
- typePerfect.narrowPublicClassMethodParamType.neon

baselines/method.nonObject.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# total 1 error
2+
3+
parameters:
4+
ignoreErrors:
5+
-
6+
message: '#^Cannot call method whenNotEmpty\(\) on Illuminate\\Support\\Collection\<int, string\>\|null\.$#'
7+
count: 1
8+
path: ../src/Foundation/Rectors/HasOptionsRector.php

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"illuminate/support": "^9.52 || ^10.0 || ^11.0 || ^12.0",
9696
"infection/extension-installer": "^0.1",
9797
"infection/infection": "^0.26",
98+
"kevinrob/guzzle-cache-middleware": "^5.1 || ^6.0 || ^7.0",
9899
"mockery/mockery": "^1.6",
99100
"nette/utils": "^4.0",
100101
"pestphp/pest": "^1.23 || ^2.0 || ^3.0 || ^4.0",

rector-php82.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
use Guanguans\Notify\Foundation\Rectors\HasHttpClientDocCommentRector;
17-
use Guanguans\Notify\Foundation\Rectors\HasOptionsDocCommentRector;
17+
use Guanguans\Notify\Foundation\Rectors\HasOptionsRector;
1818
use Guanguans\Notify\Foundation\Rectors\ToInternalExceptionRector;
1919
use Rector\Config\RectorConfig;
2020
use Rector\Php82\Rector\Param\AddSensitiveParameterAttributeRector;
@@ -33,7 +33,7 @@
3333
->withPhpVersion(PhpVersion::PHP_82)
3434
->withRules([
3535
HasHttpClientDocCommentRector::class,
36-
HasOptionsDocCommentRector::class,
36+
HasOptionsRector::class,
3737
ToInternalExceptionRector::class,
3838
])
3939
->withConfiguredRule(AddSensitiveParameterAttributeRector::class, [

rector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
use Guanguans\Notify\Foundation\Concerns\HasOptions;
2222
use Guanguans\Notify\Foundation\Method;
2323
use Guanguans\Notify\Foundation\Rectors\HasHttpClientDocCommentRector;
24-
use Guanguans\Notify\Foundation\Rectors\HasOptionsDocCommentRector;
24+
use Guanguans\Notify\Foundation\Rectors\HasOptionsRector;
2525
use Guanguans\Notify\Foundation\Rectors\ToInternalExceptionRector;
2626
use Guanguans\Notify\Foundation\Response;
2727
use GuzzleHttp\RequestOptions;
@@ -113,7 +113,7 @@
113113
StaticClosureRector::class,
114114
SortAssociativeArrayByKeyRector::class,
115115
HasHttpClientDocCommentRector::class,
116-
HasOptionsDocCommentRector::class,
116+
HasOptionsRector::class,
117117
ToInternalExceptionRector::class,
118118
])
119119
->withConfiguredRule(ChangeMethodVisibilityRector::class, [

src/Foundation/Rectors/HasOptionsDocCommentRector.php renamed to src/Foundation/Rectors/HasOptionsRector.php

Lines changed: 144 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,23 @@
1818
use Guanguans\Notify\Foundation\Message;
1919
use Guanguans\Notify\Foundation\Support\Str;
2020
use Guanguans\Notify\Foundation\Support\Utils;
21+
use Illuminate\Support\Collection;
22+
use Illuminate\Support\Pluralizer;
23+
use PhpParser\Builder\Property as PropertyBuilder;
2124
use PhpParser\Node;
25+
use PhpParser\Node\ArrayItem;
26+
use PhpParser\Node\Expr\Array_;
27+
use PhpParser\Node\Scalar\String_;
2228
use PhpParser\Node\Stmt;
2329
use PhpParser\Node\Stmt\Class_;
30+
use PhpParser\Node\Stmt\ClassMethod;
2431
use PhpParser\Node\Stmt\Property;
2532
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
2633
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
2734
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
2835
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
2936
use Rector\Contract\Rector\ConfigurableRectorInterface;
37+
use Rector\PhpParser\Parser\SimplePhpParser;
3038
use Rector\Rector\AbstractRector;
3139
use Symplify\RuleDocGenerator\Exception\PoorDocumentationException;
3240
use Symplify\RuleDocGenerator\Exception\ShouldNotHappenException;
@@ -37,15 +45,16 @@
3745
/**
3846
* @internal
3947
*/
40-
final class HasOptionsDocCommentRector extends AbstractRector implements ConfigurableRectorInterface
48+
final class HasOptionsRector extends AbstractRector implements ConfigurableRectorInterface
4149
{
4250
private array $classes = [
4351
Message::class,
4452
];
4553

4654
public function __construct(
4755
private DocBlockUpdater $docBlockUpdater,
48-
private PhpDocInfoFactory $phpDocInfoFactory
56+
private PhpDocInfoFactory $phpDocInfoFactory,
57+
private SimplePhpParser $simplePhpParser,
4958
) {}
5059

5160
/**
@@ -116,29 +125,13 @@ public function getNodeTypes(): array
116125
*/
117126
public function refactor(Node $node): ?Node
118127
{
119-
/** @var class-string $class */
120-
$class = $this->getName($node);
121-
122-
if (!$this->isSubclassesOf($class)) {
128+
if (!$this->isSubclassesOf($this->getName($node))) {
123129
return null;
124130
}
125131

132+
$this->addMethodsOfListTypeOption($node);
126133
$this->sortProperties($node);
127-
128-
if ([] === ($defined = $this->definedFor($class))) {
129-
return $node;
130-
}
131-
132-
$phpDocInfo = $this->phpDocInfoFactory->createEmpty($node);
133-
134-
/** @var array<string, mixed> $allowedTypes */
135-
$allowedTypes = (new \ReflectionClass($class))->getDefaultProperties()['allowedTypes'] ?? [];
136-
137-
foreach ($defined as $option) {
138-
$phpDocInfo->addPhpDocTagNode($this->createMethodPhpDocTagNode($option, $allowedTypes[$option] ?? null));
139-
}
140-
141-
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
134+
$this->addPhpDocTagNodesOfMethod($node);
142135

143136
return $node;
144137
}
@@ -154,6 +147,90 @@ private function isSubclassesOf(string $object): bool
154147
return false;
155148
}
156149

150+
/**
151+
* @noinspection PhpPossiblePolymorphicInvocationInspection
152+
* @noinspection D
153+
*
154+
* @throws \ReflectionException
155+
*/
156+
private function addMethodsOfListTypeOption(Class_ $class): void
157+
{
158+
collect($this->allowedTypesFor($this->getName($class)))
159+
->filter(
160+
static fn (array|string $allowedType): bool => \is_string($allowedType) && str_ends_with($allowedType, '[]')
161+
)
162+
->keys()
163+
->whenNotEmpty(function (Collection $options) use ($class): void {
164+
$property = collect($class->stmts)->first(
165+
fn (Stmt $stmt): bool => $stmt instanceof Property && $this->isName($stmt, 'options')
166+
);
167+
168+
if (!$property instanceof Property) {
169+
$class->stmts[] = (new PropertyBuilder('options'))
170+
->makeProtected()
171+
->setType('array')
172+
->setDefault($options->mapWithKeys(static fn (string $option): array => [$option => []])->all())
173+
->getNode();
174+
175+
return;
176+
}
177+
178+
$options->each(function (string $option) use ($property): void {
179+
if (!($default = $property->props[0]->default) instanceof Array_) {
180+
return;
181+
}
182+
183+
$arrayItem = collect($default->items)->first(
184+
fn (ArrayItem $arrayItem): bool => $this->isName($arrayItem->key, $option)
185+
);
186+
187+
$arrayItem instanceof ArrayItem
188+
? $arrayItem->value = $this->nodeFactory->createArray([])
189+
: $default->items[] = new ArrayItem(
190+
$this->nodeFactory->createArray([]),
191+
new String_($option)
192+
);
193+
});
194+
})
195+
->whenNotEmpty(function (Collection $options) use ($class): void {
196+
$options->each(function (string $option) use ($class): void {
197+
if (
198+
collect($class->stmts)->first(
199+
fn (Stmt $stmt): bool => $stmt instanceof ClassMethod && $this->isName(
200+
$stmt,
201+
'add'.Str::studly(Pluralizer::singular($option))
202+
)
203+
) instanceof ClassMethod
204+
) {
205+
return;
206+
}
207+
208+
/** @var list<Class_> $nodes */
209+
$nodes = $this->simplePhpParser->parseString(
210+
\sprintf(
211+
<<<'code'
212+
class Message
213+
{
214+
public function add%s(array $%s): self
215+
{
216+
$this->options['%s'][] = $%s;
217+
218+
return $this;
219+
}
220+
}
221+
code,
222+
Str::studly($singularOption = Pluralizer::singular($option)),
223+
$singularOption,
224+
$option,
225+
$singularOption,
226+
)
227+
);
228+
229+
$class->stmts[] = $nodes[0]->stmts[0];
230+
});
231+
});
232+
}
233+
157234
private function sortProperties(Class_ $class): void
158235
{
159236
usort($class->stmts, static function (Stmt $a, Stmt $b): int {
@@ -182,6 +259,25 @@ private function sortProperties(Class_ $class): void
182259
});
183260
}
184261

262+
/**
263+
* @throws \ReflectionException
264+
*/
265+
private function addPhpDocTagNodesOfMethod(Class_ $node): void
266+
{
267+
if ([] === ($defined = $this->definedFor($class = $this->getName($node)))) {
268+
return;
269+
}
270+
271+
$phpDocInfo = $this->phpDocInfoFactory->createEmpty($node);
272+
$allowedTypes = $this->allowedTypesFor($class);
273+
274+
foreach ($defined as $option) {
275+
$phpDocInfo->addPhpDocTagNode($this->createPhpDocTagNodeOfMethod($option, $allowedTypes[$option] ?? null));
276+
}
277+
278+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
279+
}
280+
185281
/**
186282
* @throws \ReflectionException
187283
*
@@ -195,24 +291,41 @@ private function definedFor(string $class): array
195291
->all();
196292
}
197293

294+
/**
295+
* @throws \ReflectionException
296+
*
297+
* @return array<string, mixed>
298+
*/
299+
private function allowedTypesFor(string $class): array
300+
{
301+
return (new \ReflectionClass($class))->getDefaultProperties()['allowedTypes'] ?? [];
302+
}
303+
198304
/**
199305
* @see \Symfony\Component\OptionsResolver\OptionsResolver::VALIDATION_FUNCTIONS
200306
*
201307
* @param list<string>|string $optionAllowedTypes
202308
*/
203-
private function createMethodPhpDocTagNode(string $option, null|array|string $optionAllowedTypes): PhpDocTagNode
309+
private function createPhpDocTagNodeOfMethod(string $option, null|array|string $optionAllowedTypes): PhpDocTagNode
204310
{
205311
$parameter = collect([
206312
collect((array) ($optionAllowedTypes ?? 'mixed'))
207-
->map(static fn (string $type): array|string => match ($type) {
208-
'boolean' => 'bool',
209-
'integer', 'long' => 'int',
210-
'double', 'real' => 'float',
211-
'numeric' => ['int', 'float'],
212-
'scalar', 'resource' => 'mixed',
213-
'countable' => '\\'.\Countable::class,
214-
default => $type,
215-
})
313+
->map(
314+
static fn (string $type): array|string => match (
315+
match (true) {
316+
str_ends_with($type, '[]') => $type = 'array',
317+
default => $type,
318+
}
319+
) {
320+
'boolean' => 'bool',
321+
'integer', 'long' => 'int',
322+
'double', 'real' => 'float',
323+
'numeric' => ['int', 'float'],
324+
'scalar', 'resource' => 'mixed',
325+
'countable' => '\\'.\Countable::class,
326+
default => $type,
327+
}
328+
)
216329
->flatten()
217330
->unique()
218331
->sort()

src/ZohoCliq/Client.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
namespace Guanguans\Notify\ZohoCliq;
1515

1616
/**
17-
* @see https://www.zoho.com/cliq/help/platform/webhook-tokens.html
1817
* @see https://cliq.zoho.com/integrations/webhook-tokens
18+
* @see https://www.zoho.com/cliq/help/platform/webhook-tokens.html
1919
* @see https://www.zoho.com/cliq/help/restapi/v2/#Post_Message_Channel
2020
* @see https://www.zoho.com/cliq/help/restapi/v2/#Message_Object
21-
* @see https://www.zoho.com/cliq/help/search-results.html?query=webhook
2221
* @see https://www.zoho.com/cliq/help/restapi/v2/#authentication
2322
* @see https://github.com/Weble/ZohoClient
2423
* @see https://github.com/MarJose123/laravel-zoho-cliq-alert
@@ -27,7 +26,7 @@
2726
* curl --location 'https://cliq.zoho.com/api/v2/channelsbyname/announcements/message?zapikey=1001.4805235707f212af4b11be76483da614.d95141b05ae0550eabe503061a598' \
2827
* --header 'Content-Type: application/json' \
2928
* --data '{
30-
* "text": "Welcome to Agile Bot! I'\''m here to give you a brief on what Agile is all about."
29+
* "text": "This is text."
3130
* }'
3231
* ```
3332
*/

src/ZohoCliq/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## Related links
66

7-
* [https://www.zoho.com/cliq/help/platform/webhook-tokens.html](https://www.zoho.com/cliq/help/platform/webhook-tokens.html)
87
* [https://cliq.zoho.com/integrations/webhook-tokens](https://cliq.zoho.com/integrations/webhook-tokens)
8+
* [https://www.zoho.com/cliq/help/platform/webhook-tokens.html](https://www.zoho.com/cliq/help/platform/webhook-tokens.html)
99
* [https://www.zoho.com/cliq/help/restapi/v2/#Post_Message_Channel](https://www.zoho.com/cliq/help/restapi/v2/#Post_Message_Channel)
1010
* [https://www.zoho.com/cliq/help/restapi/v2/#Message_Object](https://www.zoho.com/cliq/help/restapi/v2/#Message_Object)

0 commit comments

Comments
 (0)