Docblock::splitDocBlock()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 60
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 36
dl 0
loc 60
ccs 11
cts 11
cp 1
rs 9.344
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3

How to fix   Long Method   

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 declare(strict_types=1);
2
/*
3
 * This file is part of the Docblock package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license MIT License
8
 */
9
10
namespace phpowermove\docblock;
11
12
use InvalidArgumentException;
13
use LogicException;
14
use phootwork\collection\ArrayList;
15
use phootwork\collection\Map;
16
use phootwork\lang\Comparator;
17
use phpowermove\docblock\tags\AbstractTag;
18
use phpowermove\docblock\tags\TagFactory;
19
use ReflectionClass;
20
use ReflectionFunctionAbstract;
21
use ReflectionProperty;
22
23
class Docblock implements \Stringable {
24
	protected string $shortDescription;
25
	protected string $longDescription;
26
	protected ArrayList $tags;
27
	protected ?Comparator $comparator = null;
28
29
	public const REGEX_TAGNAME = '[\w\-\_\\\\]+';
30
31
	/**
32
	 * Static docblock factory
33
	 * 
34
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock a docblock to parse
35
	 *
36
	 * @return $this
37
	 */
38 1
	public static function create(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock = ''): self {
39 1
		return new static($docblock);
40
	}
41
42
	/**
43
	 * Creates a new docblock instance and parses the initial string or reflector object if given
44
	 * 
45
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock a docblock to parse
46
	 */
47 13
	final public function __construct(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock = '') {
48 13
		$this->tags = new ArrayList();
49 13
		$this->parse($docblock);
50 12
	}
51
52
	/**
53
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
54
	 *
55
	 * @param ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock
56
	 *
57
	 * @throws InvalidArgumentException if there is no getDocComment() method available
58
	 */
59 13
	protected function parse(ReflectionFunctionAbstract|ReflectionClass|ReflectionProperty|string $docblock): void {
60 13
		$docblock = is_object($docblock) ? $docblock->getDocComment() : $docblock;
61 13
		$docblock = $this->cleanInput($docblock);
62
63 13
		[$short, $long, $tags] = $this->splitDocBlock($docblock);
64 13
		$this->shortDescription = $short;
65 13
		$this->longDescription = $long;
66 13
		$this->parseTags($tags);
67 12
	}
68
69
	/**
70
	 * Strips the asterisks from the DocBlock comment.
71
	 * 
72
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
73
	 *
74
	 * @param string $comment String containing the comment text.
75
	 *
76
	 * @return string
77
	 */
78 13
	protected function cleanInput(string $comment): string {
79 13
		$comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment));
80
81
		// reg ex above is not able to remove */ from a single line docblock
82 13
		if (substr($comment, -2) == '*/') {
83 1
			$comment = trim(substr($comment, 0, -2));
84
		}
85
86
		// normalize strings
87 13
		$comment = str_replace(["\r\n", "\r"], "\n", $comment);
88
89 13
		return $comment;
90
	}
91
92
	/**
93
	 * Splits the Docblock into a short description, long description and
94
	 * block of tags.
95
	 * 
96
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
97
	 *
98
	 * @param string $comment Comment to split into the sub-parts.
99
	 *
100
	 * @author RichardJ Special thanks to RichardJ for the regex responsible
101
	 *     for the split.
102
	 *
103
	 * @return string[] containing the short-, long description and an element
104
	 *     containing the tags.
105
	 */
106 12
	protected function splitDocBlock(string $comment): array {
107 12
		$matches = [];
108
109 12
		if (str_starts_with($comment, '@')) {
110 3
			$matches = ['', '', $comment];
111
		} else {
112
			// clears all extra horizontal whitespace from the line endings
113
			// to prevent parsing issues
114 9
			$comment = preg_replace('/\h*$/Sum', '', $comment);
115
116
			/*
117
			 * Splits the docblock into a short description, long description and
118
			 * tags section
119
			 * - The short description is started from the first character until
120
			 *   a dot is encountered followed by a newline OR
121
			 *   two consecutive newlines (horizontal whitespace is taken into
122
			 *   account to consider spacing errors)
123
			 * - The long description, any character until a new line is
124
			 *   encountered followed by an @ and word characters (a tag).
125
			 *   This is optional.
126
			 * - Tags; the remaining characters
127
			 *
128
			 * Big thanks to RichardJ for contributing this Regular Expression
129
			 */
130 9
			preg_match(
131 9
				'/
132
		        \A (
133
		          [^\n.]+
134
		          (?:
135
		            (?! \. \n | \n{2} ) # disallow the first seperator here
136
		            [\n.] (?! [ \t]* @\pL ) # disallow second seperator
137
		            [^\n.]+
138
		          )*
139
		          \.?
140
		        )
141
		        (?:
142
		          \s* # first seperator (actually newlines but it\'s all whitespace)
143
		          (?! @\pL ) # disallow the rest, to make sure this one doesn\'t match,
144
		          #if it doesn\'t exist
145
		          (
146
		            [^\n]+
147
		            (?: \n+
148
		              (?! [ \t]* @\pL ) # disallow second seperator (@param)
149
		              [^\n]+
150
		            )*
151
		          )
152
		        )?
153
		        (\s+ [\s\S]*)? # everything that follows
154
		        /ux',
155
				$comment,
156
				$matches
157
			);
158 9
			array_shift($matches);
159
		}
160
161 12
		while (count($matches) < 3) {
162 7
			$matches[] = '';
163
		}
164
165 12
		return $matches;
166
	}
167
168
	/**
169
	 * Parses the tags
170
	 * 
171
	 * @see https://github.com/phpDocumentor/ReflectionDocBlock/blob/master/src/phpDocumentor/Reflection/DocBlock.php Original Method
172
	 *
173
	 * @param string $tags
174
	 *
175
	 * @throws LogicException
176
	 * @throws InvalidArgumentException
177
	 */
178 13
	protected function parseTags(string $tags): void {
179 13
		$tags = trim($tags);
180 13
		if ($tags !== '') {
181
182
			// sanitize lines
183 6
			$result = [];
184 6
			foreach (explode("\n", $tags) as $line) {
185 6
				if ($this->isTagLine($line) || count($result) == 0) {
186 6
					$result[] = $line;
187 2
				} elseif ($line !== '') {
188 1
					$result[count($result) - 1] .= "\n" . $line;
189
				}
190
			}
191
192
			// create proper Tag objects
193 6
			if (count($result)) {
194 6
				$this->tags->clear();
195 6
				foreach ($result as $line) {
196 6
					$this->tags->add($this->parseTag($line));
197
				}
198
			}
199
		}
200 12
	}
201
202
	/**
203
	 * Checks whether the given line is a tag line (= starts with @) or not
204
	 * 
205
	 * @param string $line
206
	 *
207
	 * @return bool
208
	 */
209 6
	protected function isTagLine(string $line): bool {
210 6
		return str_starts_with($line, '@');
211
	}
212
213
	/**
214
	 * Parses an individual tag line
215
	 * 
216
	 * @param string $line
217
	 *
218
	 * @throws InvalidArgumentException
219
	 *
220
	 * @return AbstractTag
221
	 */
222 6
	protected function parseTag(string $line): AbstractTag {
223 6
		$matches = [];
224 6
		if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us', $line, $matches)) {
225 1
			throw new InvalidArgumentException('Invalid tag line detected: ' . $line);
226
		}
227
228 5
		$tagName = $matches[1];
229 5
		$content = $matches[2] ?? '';
230
231 5
		return TagFactory::create($tagName, $content);
232
	}
233
234
	/**
235
	 * Returns the short description
236
	 * 
237
	 * @return string the short description
238
	 */
239 3
	public function getShortDescription(): string {
240 3
		return $this->shortDescription;
241
	}
242
243
	/**
244
	 * Sets the short description
245
	 * 
246
	 * @param string $description the new description     
247
	 *
248
	 * @return $this   	
249
	 */
250 2
	public function setShortDescription(string $description = ''): self {
251 2
		$this->shortDescription = $description;
252
253 2
		return $this;
254
	}
255
256
	/**
257
	 * Returns the long description
258
	 *
259
	 * @return string the long description
260
	 */
261 2
	public function getLongDescription(): string {
262 2
		return $this->longDescription;
263
	}
264
265
	/**
266
	 * Sets the long description
267
	 * 
268
	 * @param string $description the new description
269
	 *
270
	 * @return $this
271
	 */
272 2
	public function setLongDescription(string $description = ''): self {
273 2
		$this->longDescription = $description;
274
275 2
		return $this;
276
	}
277
278
	/**
279
	 * Adds a tag to this docblock
280
	 * 
281
	 * @param AbstractTag $tag
282
	 *
283
	 * @return $this
284
	 */
285 4
	public function appendTag(AbstractTag $tag): self {
286 4
		$this->tags->add($tag);
287
288 4
		return $this;
289
	}
290
291
	/**
292
	 * Removes tags (by tag name)
293
	 *
294
	 * @param string $tagName
295
	 */
296 1
	public function removeTags(string $tagName = ''): void {
297 1
		$this->tags = $this->tags->filter(function (AbstractTag $tag) use ($tagName): bool {
298 1
			return $tagName !== $tag->getTagName();
299 1
		});
300 1
	}
301
302
	/**
303
	 * Checks whether a tag is present
304
	 * 
305
	 * @param string $tagName
306
	 *
307
	 * @return bool
308
	 */
309 2
	public function hasTag(string $tagName): bool {
310 2
		return $this->tags->search(
311
			$tagName,
312 2
			fn (AbstractTag $tag, string $query): bool => $tag->getTagName() === $query
313 2
		);
314
	}
315
316
	/**
317
	 * Gets tags (by tag name)
318
	 * 
319
	 * @param string $tagName
320
	 *
321
	 * @return ArrayList the tags
322
	 */
323 3
	public function getTags(string $tagName = ''): ArrayList {
324 3
		return $tagName === '' ? $this->tags : $this->tags->filter(
325 1
			fn (AbstractTag $tag): bool => $tag->getTagName() === $tagName
326 3
		);
327
	}
328
329
	/**
330
	 * A list of tags sorted by tag-name
331
	 * 
332
	 * @return ArrayList
333
	 */
334 6
	public function getSortedTags(): ArrayList {
335 6
		$this->comparator = $this->comparator ?? new TagNameComparator();
336
337
		// 1) group by tag name
338 6
		$group = new Map();
339
		/** @var AbstractTag $tag */
340 6
		foreach ($this->tags->toArray() as $tag) {
341 5
			if (!$group->has($tag->getTagName())) {
342 5
				$group->set($tag->getTagName(), new ArrayList());
343
			}
344
345
			/** @var ArrayList $list */
346 5
			$list = $group->get($tag->getTagName());
347 5
			$list->add($tag);
348
		}
349
350
		// 2) Sort the group by tag name
351 6
		$group->sortKeys(new TagNameComparator());
352
353
		// 3) flatten the group
354 6
		$sorted = new ArrayList();
355
		/** @var array $tags */
356 6
		foreach ($group->values()->toArray() as $tags) {
357 5
			$sorted->add(...$tags);
358
		}
359
360 6
		return $sorted;
361
	}
362
363
	/**
364
	 * Returns true when there is no content in the docblock
365
	 *  
366
	 * @return bool
367
	 */
368 1
	public function isEmpty(): bool {
369 1
		return $this->shortDescription === ''
370 1
				&& $this->longDescription === ''
371 1
				&& $this->tags->size() === 0;
372
	}
373
374
	/**
375
	 * Returns the string version of the docblock
376
	 * 
377
	 * @return string
378
	 */
379 5
	public function toString(): string {
380 5
		$docblock = "/**\n";
381
382
		// short description
383 5
		$short = trim($this->shortDescription);
384 5
		if ($short !== '') {
385 3
			$docblock .= $this->writeLines(explode("\n", $short));
386
		}
387
388
		// short description
389 5
		$long = trim($this->longDescription);
390 5
		if ($long !== '') {
391 2
			$docblock .= $this->writeLines(explode("\n", $long), !empty($short));
392
		}
393
394
		// tags
395 5
		$tags = $this->getSortedTags()->map(function (AbstractTag $tag): string {
396 4
			return (string) $tag;
397 5
		});
398
399 5
		if (!$tags->isEmpty()) {
400
			/** @psalm-suppress MixedArgumentTypeCoercion */
401 4
			$docblock .= $this->writeLines($tags->toArray(), $short !== '' || $long !== '');
402
		}
403
404 5
		$docblock .= ' */';
405
406 5
		return $docblock;
407
	}
408
409
	/**
410
	 * Writes multiple lines with ' * ' prefixed for docblock
411
	 * 
412
	 * @param string[] $lines the lines to be written
413
	 * @param bool $newline if a new line should be added before
414
	 *
415
	 * @return string the lines as string
416
	 */
417 5
	protected function writeLines(array $lines, bool $newline = false): string {
418 5
		$docblock = '';
419 5
		if ($newline) {
420 3
			$docblock .= " *\n";
421
		}
422
423 5
		foreach ($lines as $line) {
424 5
			if (str_contains($line, "\n")) {
425 1
				$sublines = explode("\n", $line);
426 1
				$line = array_shift($sublines);
427 1
				$docblock .= " * $line\n";
428 1
				$docblock .= $this->writeLines($sublines);
429
			} else {
430 5
				$docblock .= " * $line\n";
431
			}
432
		}
433
434 5
		return $docblock;
435
	}
436
437
	/**
438
	 * Magic toString() method
439
	 * 
440
	 * @return string
441
	 */
442 1
	public function __toString(): string {
443 1
		return $this->toString();
444
	}
445
}
446