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