1818use Guanguans \Notify \Foundation \Message ;
1919use Guanguans \Notify \Foundation \Support \Str ;
2020use Guanguans \Notify \Foundation \Support \Utils ;
21+ use Illuminate \Support \Collection ;
22+ use Illuminate \Support \Pluralizer ;
23+ use PhpParser \Builder \Property as PropertyBuilder ;
2124use PhpParser \Node ;
25+ use PhpParser \Node \ArrayItem ;
26+ use PhpParser \Node \Expr \Array_ ;
27+ use PhpParser \Node \Scalar \String_ ;
2228use PhpParser \Node \Stmt ;
2329use PhpParser \Node \Stmt \Class_ ;
30+ use PhpParser \Node \Stmt \ClassMethod ;
2431use PhpParser \Node \Stmt \Property ;
2532use PHPStan \PhpDocParser \Ast \PhpDoc \GenericTagValueNode ;
2633use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
2734use Rector \BetterPhpDocParser \PhpDocInfo \PhpDocInfoFactory ;
2835use Rector \Comments \NodeDocBlock \DocBlockUpdater ;
2936use Rector \Contract \Rector \ConfigurableRectorInterface ;
37+ use Rector \PhpParser \Parser \SimplePhpParser ;
3038use Rector \Rector \AbstractRector ;
3139use Symplify \RuleDocGenerator \Exception \PoorDocumentationException ;
3240use Symplify \RuleDocGenerator \Exception \ShouldNotHappenException ;
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 ()
0 commit comments