Completed
Push — readme-redesign ( e2fd40...17eb05 )
by Alexander
108:51 queued 68:52
created

PhpDocController::fixLineSpacing()   F

Complexity

Conditions 42
Paths > 20000

Size

Total Lines 110
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 110
rs 2
c 0
b 0
f 0
cc 42
eloc 75
nc 26082
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\build\controllers;
9
10
use Yii;
11
use yii\console\Controller;
12
use yii\helpers\Console;
13
use yii\helpers\FileHelper;
14
15
/**
16
 * PhpDocController is there to help maintaining PHPDoc annotation in class files
17
 *
18
 * @author Carsten Brandt <[email protected]>
19
 * @author Alexander Makarov <[email protected]>
20
 * @since 2.0
21
 */
22
class PhpDocController extends Controller
23
{
24
    public $defaultAction = 'property';
25
    /**
26
     * @var bool whether to update class docs directly. Setting this to false will just output docs
27
     * for copy and paste.
28
     */
29
    public $updateFiles = true;
30
    /**
31
     * @var bool whether to add copyright header to php files. This should be skipped in application code.
32
     */
33
    public $skipFrameworkRequirements = false;
34
35
36
    /**
37
     * Generates `@property` annotations in class files from getters and setters
38
     *
39
     * Property description will be taken from getter or setter or from an `@property` annotation
40
     * in the getters docblock if there is one defined.
41
     *
42
     * See https://github.com/yiisoft/yii2/wiki/Core-framework-code-style#documentation for details.
43
     *
44
     * @param string $root the directory to parse files from. Defaults to YII2_PATH.
45
     */
46
    public function actionProperty($root = null)
47
    {
48
        $files = $this->findFiles($root);
49
50
        $nFilesTotal = 0;
51
        $nFilesUpdated = 0;
52
        foreach ($files as $file) {
53
            $result = $this->generateClassPropertyDocs($file);
54
            if ($result !== false) {
55
                list($className, $phpdoc) = $result;
56
                if ($this->updateFiles) {
57
                    if ($this->updateClassPropertyDocs($file, $className, $phpdoc)) {
58
                        $nFilesUpdated++;
59
                    }
60
                } elseif (!empty($phpdoc)) {
61
                    $this->stdout("\n[ " . $file . " ]\n\n", Console::BOLD);
62
                    $this->stdout($phpdoc);
63
                }
64
            }
65
            $nFilesTotal++;
66
        }
67
68
        $this->stdout("\nParsed $nFilesTotal files.\n");
69
        $this->stdout("Updated $nFilesUpdated files.\n");
70
    }
71
72
    /**
73
     * Fix some issues with PHPdoc in files
74
     *
75
     * @param string $root the directory to parse files from. Defaults to YII2_PATH.
76
     */
77
    public function actionFix($root = null)
78
    {
79
        $files = $this->findFiles($root, false);
80
81
        $nFilesTotal = 0;
82
        $nFilesUpdated = 0;
83
        foreach ($files as $file) {
84
            $contents = file_get_contents($file);
85
            $sha = sha1($contents);
86
87
            // fix line endings
88
            $lines = preg_split('/(\r\n|\n|\r)/', $contents);
89
90
            if (!$this->skipFrameworkRequirements) {
91
                $this->fixFileDoc($lines);
92
            }
93
            $this->fixDocBlockIndentation($lines);
94
            $lines = array_values($this->fixLineSpacing($lines));
95
96
            $newContent = implode("\n", $lines);
97
            if ($sha !== sha1($newContent)) {
98
                $nFilesUpdated++;
99
            }
100
            file_put_contents($file, $newContent);
101
            $nFilesTotal++;
102
        }
103
104
        $this->stdout("\nParsed $nFilesTotal files.\n");
105
        $this->stdout("Updated $nFilesUpdated files.\n");
106
107
    }
108
109
    /**
110
     * @inheritdoc
111
     */
112
    public function options($actionID)
113
    {
114
        return array_merge(parent::options($actionID), ['updateFiles', 'skipFrameworkRequirements']);
115
    }
116
117
    protected function findFiles($root, $needsInclude = true)
118
    {
119
        $except = [];
120
        if ($needsInclude) {
121
            $extensionExcept = [
122
                'apidoc' => [
123
                    '/helpers/PrettyPrinter.php',
124
                    '/extensions/apidoc/helpers/ApiIndexer.php',
125
                    '/extensions/apidoc/helpers/ApiMarkdownLaTeX.php',
126
                ],
127
                'codeception' => [
128
                    '/TestCase.php',
129
                    '/DbTestCase.php',
130
                ],
131
                'gii' => [
132
                    '/components/DiffRendererHtmlInline.php',
133
                    '/generators/extension/default/AutoloadExample.php',
134
                ],
135
                'swiftmailer' => [
136
                    '/Logger.php',
137
                ],
138
                'twig' => [
139
                    '/Extension.php',
140
                    '/Optimizer.php',
141
                    '/Template.php',
142
                    '/TwigSimpleFileLoader.php',
143
                    '/ViewRendererStaticClassProxy.php',
144
                ],
145
            ];
146
        } else {
147
            $extensionExcept = [];
148
        }
149
150
        if ($root === null) {
151
            $root = dirname(YII2_PATH);
152
            $extensionPath = "$root/extensions";
153
            foreach (scandir($extensionPath) as $extension) {
154
                if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
155
                    Yii::setAlias("@yii/$extension", "$extensionPath/$extension");
156
                }
157
            }
158
159
            $except = [
160
                '/apps/',
161
                '/build/',
162
                '/docs/',
163
                '/extensions/composer/',
164
                '/framework/BaseYii.php',
165
                '/framework/Yii.php',
166
                'assets/',
167
                'tests/',
168
                'vendor/',
169
            ];
170
            foreach($extensionExcept as $ext => $paths) {
171
                foreach($paths as $path) {
172
                    $except[] = "/extensions/$ext$path";
173
                }
174
            }
175
        } elseif (preg_match('~extensions/([\w-]+)[\\\\/]?$~', $root, $matches)) {
176
177
            $extensionPath = dirname(rtrim($root, '\\/'));
178
            $this->setUpExtensionAliases($extensionPath);
179
180
            list(, $extension) = $matches;
181
            Yii::setAlias("@yii/$extension", "$root");
182
            if (is_file($autoloadFile = Yii::getAlias("@yii/$extension/vendor/autoload.php"))) {
183
                include($autoloadFile);
184
            }
185
186
            if (isset($extensionExcept[$extension])) {
187
                foreach($extensionExcept[$extension] as $path) {
188
                    $except[] = $path;
189
                }
190
            }
191
            $except[] = '/vendor/';
192
            $except[] = '/tests/';
193
            $except[] = '/docs/';
194
195
//            // composer extension does not contain yii code
196
//            if ($extension === 'composer') {
197
//                return [];
198
//            }
199
        } elseif (preg_match('~apps/([\w-]+)[\\\\/]?$~', $root, $matches)) {
200
201
            $extensionPath = dirname(dirname(rtrim($root, '\\/'))) . '/extensions';
202
            $this->setUpExtensionAliases($extensionPath);
203
204
            list(, $appName) = $matches;
205
            Yii::setAlias("@app-$appName", "$root");
206
            if (is_file($autoloadFile = Yii::getAlias("@app-$appName/vendor/autoload.php"))) {
207
                include($autoloadFile);
208
            }
209
210
            $except[] = '/runtime/';
211
            $except[] = '/vendor/';
212
            $except[] = '/tests/';
213
            $except[] = '/docs/';
214
215
        }
216
        $root = FileHelper::normalizePath($root);
217
        $options = [
218
            'filter' => function ($path) {
219
                    if (is_file($path)) {
220
                        $file = basename($path);
221
                        if ($file[0] < 'A' || $file[0] > 'Z') {
222
                            return false;
223
                        }
224
                    }
225
226
                    return null;
227
                },
228
            'only' => ['*.php'],
229
            'except' => array_merge($except, [
230
                '.git/',
231
                'views/',
232
                'requirements/',
233
                'gii/generators/',
234
                'vendor/',
235
            ]),
236
        ];
237
        return FileHelper::findFiles($root, $options);
238
    }
239
240
    private function setUpExtensionAliases($extensionPath)
241
    {
242
        foreach (scandir($extensionPath) as $extension) {
243
            if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
244
                Yii::setAlias("@yii/$extension", "$extensionPath/$extension");
245
            }
246
        }
247
    }
248
249
    /**
250
     * Fix file PHPdoc
251
     */
252
    protected function fixFileDoc(&$lines)
253
    {
254
        // find namespace
255
        $namespace = false;
256
        $namespaceLine = '';
257
        $contentAfterNamespace = false;
258
        foreach($lines as $i => $line) {
259
            $line = trim($line);
260
            if (!empty($line)) {
261
                if (strncmp($line, 'namespace', 9) === 0) {
262
                    $namespace = $i;
263
                    $namespaceLine = $line;
264
                } elseif ($namespace !== false) {
265
                    $contentAfterNamespace = $i;
266
                    break;
267
                }
268
            }
269
        }
270
271
        if ($namespace !== false && $contentAfterNamespace !== false) {
272
            while($contentAfterNamespace > 0) {
273
                array_shift($lines);
274
                $contentAfterNamespace--;
275
            }
276
            $lines = array_merge([
277
                "<?php",
278
                "/**",
279
                " * @link http://www.yiiframework.com/",
280
                " * @copyright Copyright (c) 2008 Yii Software LLC",
281
                " * @license http://www.yiiframework.com/license/",
282
                " */",
283
                "",
284
                $namespaceLine,
285
                ""
286
            ], $lines);
287
        }
288
    }
289
290
    /**
291
     * Markdown aware fix of whitespace issues in doc comments
292
     */
293
    protected function fixDocBlockIndentation(&$lines)
294
    {
295
        $docBlock = false;
296
        $codeBlock = false;
297
        $listIndent = '';
298
        $tag = false;
299
        $indent = '';
300
        foreach($lines as $i => $line) {
301
            if (preg_match('~^(\s*)/\*\*$~', $line, $matches)) {
302
                $docBlock = true;
303
                $indent = $matches[1];
304
            } elseif (preg_match('~^(\s*)\*+/~', $line)) {
305
                if ($docBlock) { // could be the end of normal comment
306
                    $lines[$i] = $indent . ' */';
307
                }
308
                $docBlock = false;
309
                $codeBlock = false;
310
                $listIndent = '';
311
                $tag = false;
312
            } elseif ($docBlock) {
313
                $line = ltrim($line);
314
                if (isset($line[0]) && $line[0] === '*') {
315
                    $line = substr($line, 1);
316
                }
317
                if (isset($line[0]) && $line[0] === ' ') {
318
                    $line = substr($line, 1);
319
                }
320
                $docLine = str_replace("\t", '    ', rtrim($line));
321
                if (empty($docLine)) {
322
                    $listIndent = '';
323
                } elseif ($docLine[0] === '@') {
324
                    $listIndent = '';
325
                    $codeBlock = false;
326
                    $tag = true;
327
                    $docLine = preg_replace('/\s+/', ' ', $docLine);
328
                    $docLine = $this->fixParamTypes($docLine);
329
                } elseif (preg_match('/^(~~~|```)/', $docLine)) {
330
                    $codeBlock = !$codeBlock;
331
                    $listIndent = '';
332
                } elseif (preg_match('/^(\s*)([0-9]+\.|-|\*|\+) /', $docLine, $matches)) {
333
                    $listIndent = str_repeat(' ', strlen($matches[0]));
334
                    $tag = false;
335
                    $lines[$i] = $indent . ' * ' . $docLine;
336
                    continue;
337
                }
338
                if ($codeBlock) {
339
                    $lines[$i] = rtrim($indent . ' * ' . $docLine);
340
                } else {
341
                    $lines[$i] = rtrim($indent . ' * ' . (empty($listIndent) && !$tag ? $docLine : ($listIndent . ltrim($docLine))));
342
                }
343
            }
344
        }
345
    }
346
347
    protected function fixParamTypes($line)
348
    {
349
        return preg_replace_callback('~@(param|return) ([\w\\|]+)~i', function($matches) {
350
            $types = explode('|', $matches[2]);
351
            foreach($types as $i => $type) {
352
                switch($type){
353
                    case 'integer': $types[$i] = 'int'; break;
354
                    case 'boolean': $types[$i] = 'bool'; break;
355
                }
356
            }
357
            return '@' . $matches[1] . ' ' . implode('|', $types);
358
        }, $line);
359
    }
360
361
    /**
362
     * Fixes line spacing code style for properties and constants
363
     */
364
    protected function fixLineSpacing($lines)
365
    {
366
        $propertiesOnly = false;
367
        // remove blank lines between properties
368
        $skip = true;
369
        $level = 0;
370
        foreach($lines as $i => $line) {
371
            if (strpos($line, 'class ') !== false) {
372
                $skip = false;
373
            }
374
            if ($skip) {
375
                continue;
376
            }
377
378
            // keep spaces in multi line arrays
379
            if (strpos($line, '*') === false && strncmp(trim($line), "'SQLSTATE[", 10) !== 0) {
380
                $level += substr_count($line, '[') - substr_count($line, ']');
381
            }
382
383
            if (trim($line) === '') {
384
                if ($level == 0) {
385
                    unset($lines[$i]);
386
                }
387
            } elseif (ltrim($line)[0] !== '*' && strpos($line, 'function ') !== false) {
388
                break;
389
            } elseif (trim($line) === '}') {
390
                $propertiesOnly = true;
391
                break;
392
            }
393
        }
394
        $lines = array_values($lines);
395
396
        // add back some
397
        $endofUse = false;
398
        $endofConst = false;
399
        $endofPublic = false;
400
        $endofProtected = false;
401
        $endofPrivate = false;
402
        $skip = true;
403
        $level = 0; // track array properties
404
        $property = '';
405
        foreach($lines as $i => $line) {
406
            if (strpos($line, 'class ') !== false) {
407
                $skip = false;
408
            }
409
            if ($skip) {
410
                continue;
411
            }
412
413
            // check for multi line array
414
            if ($level > 0) {
415
                ${'endof'.$property} = $i;
416
            }
417
418
            $line = trim($line);
419
            if (strncmp($line, 'public $', 8) === 0 || strncmp($line, 'public static $', 15) === 0) {
420
                $endofPublic = $i;
421
                $property = 'Public';
422
                $level = 0;
423
            } elseif (strncmp($line, 'protected $', 11) === 0 || strncmp($line, 'protected static $', 18) === 0) {
424
                $endofProtected = $i;
425
                $property = 'Protected';
426
                $level = 0;
427
            } elseif (strncmp($line, 'private $', 9) === 0 || strncmp($line, 'private static $', 16) === 0) {
428
                $endofPrivate = $i;
429
                $property = 'Private';
430
                $level = 0;
431
            } elseif (substr($line,0 , 6) === 'const ') {
432
                $endofConst = $i;
433
                $property = false;
434
            } elseif (substr($line,0 , 4) === 'use ') {
435
                $endofUse = $i;
436
                $property = false;
437
            } elseif (!empty($line) && $line[0] === '*') {
438
                $property = false;
439
            } elseif (!empty($line) && $line[0] !== '*' && strpos($line, 'function ') !== false || $line === '}') {
440
                break;
441
            }
442
443
            // check for multi line array
444
            if ($property !== false && strncmp($line, "'SQLSTATE[", 10) !== 0) {
445
                $level += substr_count($line, '[') - substr_count($line, ']');
446
            }
447
        }
448
449
        $endofAll = false;
450
        foreach(['Private', 'Protected', 'Public', 'Const', 'Use'] as $var) {
451
            if (${'endof'.$var} !== false) {
452
                $endofAll = ${'endof'.$var};
453
                break;
454
            }
455
        }
456
457
//        $this->checkPropertyOrder($lineInfo);
458
        $result = [];
459
        foreach($lines as $i => $line) {
460
            $result[] = $line;
461
            if (!($propertiesOnly && $i === $endofAll)) {
462
                if ($i === $endofUse || $i === $endofConst || $i === $endofPublic ||
463
                    $i === $endofProtected || $i === $endofPrivate) {
464
                    $result[] = '';
465
                }
466
                if ($i === $endofAll) {
467
                    $result[] = '';
468
                }
469
            }
470
        }
471
472
        return $result;
473
    }
474
475
    protected function checkPropertyOrder($lineInfo)
0 ignored issues
show
Unused Code introduced by
The parameter $lineInfo is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
476
    {
477
        // TODO
478
    }
479
480
    protected function updateClassPropertyDocs($file, $className, $propertyDoc)
481
    {
482
        try {
483
            $ref = new \ReflectionClass($className);
484
        } catch (\Exception $e) {
485
            $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED);
486
            return false;
487
        }
488
        if ($ref->getFileName() != $file) {
489
            $this->stderr("[ERR] Unable to create ReflectionClass for class: $className loaded class is not from file: $file\n", Console::FG_RED);
490
            return false;
491
        }
492
493
        if (!$ref->isSubclassOf('yii\base\Object') && $className != 'yii\base\Object') {
494
            $this->stderr("[INFO] Skipping class $className as it is not a subclass of yii\\base\\Object.\n", Console::FG_BLUE, Console::BOLD);
495
            return false;
496
        }
497
498
        if ($ref->isSubclassOf('yii\db\BaseActiveRecord')) {
499
            $this->stderr("[INFO] Skipping class $className as it is an ActiveRecord class, property handling is not supported yet.\n", Console::FG_BLUE, Console::BOLD);
500
            return false;
501
        }
502
503
        $oldDoc = $ref->getDocComment();
504
        $newDoc = $this->cleanDocComment($this->updateDocComment($oldDoc, $propertyDoc));
505
506
        $seenSince = false;
507
        $seenAuthor = false;
508
509
        // TODO move these checks to different action
510
        $lines = explode("\n", $newDoc);
511
        $firstLine = trim($lines[1]);
512
        if ($firstLine === '*' || strncmp($firstLine, '* @', 3) === 0) {
513
            $this->stderr("[WARN] Class $className has no short description.\n", Console::FG_YELLOW, Console::BOLD);
514
        }
515
        foreach ($lines as $line) {
516
            $line = trim($line);
517
            if (strncmp($line, '* @since ', 9) === 0) {
518
                $seenSince = true;
519
            } elseif (strncmp($line, '* @author ', 10) === 0) {
520
                $seenAuthor = true;
521
            }
522
        }
523
524
        if (!$this->skipFrameworkRequirements) {
525
            if (!$seenSince) {
526
                $this->stderr("[ERR] No @since found in class doc in file: $file\n", Console::FG_RED);
527
            }
528
            if (!$seenAuthor) {
529
                $this->stderr("[ERR] No @author found in class doc in file: $file\n", Console::FG_RED);
530
            }
531
        }
532
533
        if (trim($oldDoc) != trim($newDoc)) {
534
535
            $fileContent = explode("\n", file_get_contents($file));
536
            $start = $ref->getStartLine() - 2;
537
            $docStart = $start - count(explode("\n", $oldDoc)) + 1;
538
539
            $newFileContent = [];
540
            $n = count($fileContent);
541
            for ($i = 0; $i < $n; $i++) {
542
                if ($i > $start || $i < $docStart) {
543
                    $newFileContent[] = $fileContent[$i];
544
                } else {
545
                    $newFileContent[] = trim($newDoc);
546
                    $i = $start;
547
                }
548
            }
549
550
            file_put_contents($file, implode("\n", $newFileContent));
551
552
            return true;
553
        }
554
555
        return false;
556
    }
557
558
    /**
559
     * remove multi empty lines and trim trailing whitespace
560
     *
561
     * @param $doc
562
     * @return string
563
     */
564
    protected function cleanDocComment($doc)
565
    {
566
        $lines = explode("\n", $doc);
567
        $n = count($lines);
568
        for ($i = 0; $i < $n; $i++) {
569
            $lines[$i] = rtrim($lines[$i]);
570
            if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') {
571
                unset($lines[$i]);
572
            }
573
        }
574
575
        return implode("\n", $lines);
576
    }
577
578
    /**
579
     * Replace property annotations in doc comment
580
     * @param $doc
581
     * @param $properties
582
     * @return string
583
     */
584
    protected function updateDocComment($doc, $properties)
585
    {
586
        $lines = explode("\n", $doc);
587
        $propertyPart = false;
588
        $propertyPosition = false;
589
        foreach ($lines as $i => $line) {
590
            $line = trim($line);
591
            if (strncmp($line, '* @property ', 12) === 0) {
592
                $propertyPart = true;
593
            } elseif ($propertyPart && $line == '*') {
594
                $propertyPosition = $i;
595
                $propertyPart = false;
596
            }
597
            if (strncmp($line, '* @author ', 10) === 0 && $propertyPosition === false) {
598
                $propertyPosition = $i - 1;
599
                $propertyPart = false;
600
            }
601
            if ($propertyPart) {
602
                unset($lines[$i]);
603
            }
604
        }
605
606
        // if no properties or other tags where present add properties at the end
607
        if ($propertyPosition === false) {
608
            $propertyPosition = count($lines) - 2;
609
        }
610
611
        $finalDoc = '';
612
        foreach ($lines as $i => $line) {
613
            $finalDoc .= $line . "\n";
614
            if ($i == $propertyPosition) {
615
                $finalDoc .= $properties;
616
            }
617
        }
618
619
        return $finalDoc;
620
    }
621
622
    protected function generateClassPropertyDocs($fileName)
623
    {
624
        $phpdoc = "";
625
        $file = str_replace("\r", "", str_replace("\t", " ", file_get_contents($fileName, true)));
626
        $ns = $this->match('#\nnamespace (?<name>[\w\\\\]+);\n#', $file);
627
        $namespace = reset($ns);
628
        $namespace = $namespace['name'];
629
        $classes = $this->match('#\n(?:abstract )?class (?<name>\w+)( extends .+)?( implements .+)?\n\{(?<content>.*)\n\}(\n|$)#', $file);
630
631
        if (count($classes) > 1) {
632
            $this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED);
633
634
            return false;
635
        }
636
        if (count($classes) < 1) {
637
            $interfaces = $this->match('#\ninterface (?<name>\w+)( extends .+)?\n\{(?<content>.*)\n\}(\n|$)#', $file);
638
            if (count($interfaces) == 1) {
639
                return false;
640
            } elseif (count($interfaces) > 1) {
641
                $this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED);
642
            } else {
643
                $traits = $this->match('#\ntrait (?<name>\w+)\n\{(?<content>.*)\n\}(\n|$)#', $file);
644
                if (count($traits) == 1) {
645
                    return false;
646
                } elseif (count($traits) > 1) {
647
                    $this->stderr("[ERR] There should be only one class/trait/interface in a file: $fileName\n", Console::FG_RED);
648
                } else {
649
                    $this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED);
650
                }
651
            }
652
653
            return false;
654
        }
655
656
        $className = null;
657
        foreach ($classes as &$class) {
658
659
            $className = $namespace . '\\' . $class['name'];
660
661
            $gets = $this->match(
662
                '#\* @return (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' .
663
                '[\s\n]{2,}public function (?<kind>get)(?<name>\w+)\((?:,? ?\$\w+ ?= ?[^,]+)*\)#',
664
                $class['content'], true);
665
            $sets = $this->match(
666
                '#\* @param (?<type>[\w\\|\\\\\\[\\]]+) \$\w+(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' .
667
                '[\s\n]{2,}public function (?<kind>set)(?<name>\w+)\(\$\w+(?:, ?\$\w+ ?= ?[^,]+)*\)#',
668
                $class['content'], true);
669
            // check for @property annotations in getter and setter
670
            $properties = $this->match(
671
                '#\* @(?<kind>property) (?<type>[\w\\|\\\\\\[\\]]+)(?: (?<comment>(?:(?!\*/|\* @).)+?)(?:(?!\*/).)+|[\s\n]*)\*/' .
672
                '[\s\n]{2,}public function [g|s]et(?<name>\w+)\(((?:,? ?\$\w+ ?= ?[^,]+)*|\$\w+(?:, ?\$\w+ ?= ?[^,]+)*)\)#',
673
                $class['content']);
674
            $acrs = array_merge($properties, $gets, $sets);
675
676
            $props = [];
677
            foreach ($acrs as &$acr) {
678
                $acr['name'] = lcfirst($acr['name']);
679
                $acr['comment'] = trim(preg_replace('#(^|\n)\s+\*\s?#', '$1 * ', $acr['comment']));
680
                $props[$acr['name']][$acr['kind']] = [
681
                    'type' => $acr['type'],
682
                    'comment' => $this->fixSentence($acr['comment']),
683
                ];
684
            }
685
686
            ksort($props);
687
688
            if (count($props) > 0) {
689
                $phpdoc .= " *\n";
690
                foreach ($props as $propName => &$prop) {
691
                    $docline = ' * @';
692
                    $docline .= 'property'; // Do not use property-read and property-write as few IDEs support complex syntax.
693
                    $note = '';
694
                    if (isset($prop['get']) && isset($prop['set'])) {
695
                        if ($prop['get']['type'] != $prop['set']['type']) {
696
                            $note = ' Note that the type of this property differs in getter and setter.'
697
                                  . ' See [[get' . ucfirst($propName) . '()]] and [[set' . ucfirst($propName) . '()]] for details.';
698
                        }
699
                    } elseif (isset($prop['get'])) {
700
                        // check if parent class has setter defined
701
                        $c = $className;
702
                        $parentSetter = false;
703
                        while ($parent = get_parent_class($c)) {
704
                            if (method_exists($parent, 'set' . ucfirst($propName))) {
705
                                $parentSetter = true;
706
                                break;
707
                            }
708
                            $c = $parent;
709
                        }
710
                        if (!$parentSetter) {
711
                            $note = ' This property is read-only.';
712
//							$docline .= '-read';
713
                        }
714
                    } elseif (isset($prop['set'])) {
715
                        // check if parent class has getter defined
716
                        $c = $className;
717
                        $parentGetter = false;
718
                        while ($parent = get_parent_class($c)) {
719
                            if (method_exists($parent, 'set' . ucfirst($propName))) {
720
                                $parentGetter = true;
721
                                break;
722
                            }
723
                            $c = $parent;
724
                        }
725
                        if (!$parentGetter) {
726
                            $note = ' This property is write-only.';
727
//							$docline .= '-write';
728
                        }
729
                    } else {
730
                        continue;
731
                    }
732
                    $docline .= ' ' . $this->getPropParam($prop, 'type') . " $$propName ";
733
                    $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note);
734
                    foreach ($comment as &$cline) {
735
                        $cline = ltrim($cline, '* ');
736
                    }
737
                    $docline = wordwrap($docline . implode(' ', $comment), 110, "\n * ") . "\n";
738
739
                    $phpdoc .= $docline;
740
                }
741
                $phpdoc .= " *\n";
742
            }
743
        }
744
745
        return [$className, $phpdoc];
746
    }
747
748
    protected function match($pattern, $subject, $split = false)
749
    {
750
        $sets = [];
751
        // split subject by double newlines because regex sometimes has problems with matching
752
        // in the complete set of methods
753
        // example: yii\di\ServiceLocator setComponents() is not recognized in the whole but in
754
        // a part of the class.
755
        $parts = $split ? explode("\n\n", $subject) : [$subject];
756
        foreach($parts as $part) {
757
            preg_match_all($pattern . 'suU', $part, $matches, PREG_SET_ORDER);
758
            foreach ($matches as &$set) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
759
                foreach ($set as $i => $match)
760
                    if (is_numeric($i) /*&& $i != 0*/)
761
                        unset($set[$i]);
762
763
                $sets[] = $set;
764
            }
765
        }
766
        return $sets;
767
    }
768
769
    protected function fixSentence($str)
770
    {
771
        // TODO fix word wrap
772
        if ($str == '')
773
            return '';
774
        return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[strlen($str) - 1] != '.' ? '.' : '');
775
    }
776
777
    protected function getPropParam($prop, $param)
778
    {
779
        return isset($prop['property']) ? $prop['property'][$param] : (isset($prop['get']) ? $prop['get'][$param] : $prop['set'][$param]);
780
    }
781
}
782