1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @copyright Copyright (c) 2017 Julius Härtl <[email protected]> |
4
|
|
|
* |
5
|
|
|
* @author Julius Härtl <[email protected]> |
6
|
|
|
* |
7
|
|
|
* @license GNU AGPL version 3 or any later version |
8
|
|
|
* |
9
|
|
|
* This program is free software: you can redistribute it and/or modify |
10
|
|
|
* it under the terms of the GNU Affero General Public License as |
11
|
|
|
* published by the Free Software Foundation, either version 3 of the |
12
|
|
|
* License, or (at your option) any later version. |
13
|
|
|
* |
14
|
|
|
* This program is distributed in the hope that it will be useful, |
15
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
16
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17
|
|
|
* GNU Affero General Public License for more details. |
18
|
|
|
* |
19
|
|
|
* You should have received a copy of the GNU Affero General Public License |
20
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
21
|
|
|
* |
22
|
|
|
*/ |
23
|
|
|
|
24
|
|
|
namespace JuliusHaertl\PHPDocToRst\Builder; |
25
|
|
|
|
26
|
|
|
use JuliusHaertl\PHPDocToRst\Extension\Extension; |
27
|
|
|
use phpDocumentor\Reflection\DocBlock; |
28
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\Param; |
29
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\Return_; |
30
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\See; |
31
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\Since; |
32
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\Throws; |
33
|
|
|
use phpDocumentor\Reflection\Element; |
34
|
|
|
use phpDocumentor\Reflection\Php\Argument; |
35
|
|
|
use phpDocumentor\Reflection\Php\Class_; |
36
|
|
|
use phpDocumentor\Reflection\Php\Constant; |
37
|
|
|
use phpDocumentor\Reflection\Php\Function_; |
38
|
|
|
use phpDocumentor\Reflection\Php\Interface_; |
39
|
|
|
use phpDocumentor\Reflection\Php\Method; |
40
|
|
|
use phpDocumentor\Reflection\Php\Property; |
41
|
|
|
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated; |
42
|
|
|
use phpDocumentor\Reflection\Php\Trait_; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Class to build reStructuredText file with sphinxcontrib-phpdomain syntax |
46
|
|
|
* |
47
|
|
|
* @package JuliusHaertl\PHPDocToRst\Builder |
48
|
|
|
*/ |
49
|
|
|
class PhpDomainBuilder extends RstBuilder { |
50
|
|
|
|
51
|
|
|
const SECTION_BEFORE_DESCRIPTION = self::class . '::SECTION_BEFORE_DESCRIPTION'; |
52
|
|
|
const SECTION_AFTER_DESCRIPTION = self::class . '::SECTION_AFTER_DESCRIPTION'; |
53
|
|
|
const SECTION_AFTER_TITLE = self::class . '::SECTION_AFTER_TITLE'; |
54
|
|
|
const SECTION_AFTER_INTRODUCTION = self::class . '::SECTION_AFTER_INTRODUCTION'; |
55
|
|
|
|
56
|
|
|
use ExtensionBuilder { |
57
|
|
|
ExtensionBuilder::__construct as private __extensionConstructor; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
public function __construct($extensions) { |
61
|
|
|
$this->__extensionConstructor($extensions); |
62
|
|
|
$this->addMultiline('.. role:: php(code)' . PHP_EOL . ':language: php', true); |
63
|
|
|
$this->addLine(); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Strip element name from Fqsen to return the namespace only |
68
|
|
|
* |
69
|
|
|
* @param Element $element |
70
|
|
|
* @return mixed |
71
|
|
|
*/ |
72
|
|
|
public static function getNamespace(Element $element) { |
73
|
|
|
return substr($element->getFqsen(), 0, strlen($element->getFqsen())-strlen('\\'. $element->getName())); |
74
|
|
|
//return str_replace('\\' . $element->getName(), '', $element->getFqsen()); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Add namespace |
79
|
|
|
* @param Element $element |
80
|
|
|
*/ |
81
|
|
|
protected function addPageHeader(Element $element) { |
82
|
|
|
$this->addH1(self::escape($element->getName()))->addLine(); |
83
|
|
|
if (self::getNamespace($element) !== '') { |
84
|
|
|
$this->beginPhpDomain('namespace', substr(self::getNamespace($element), 1), false); |
85
|
|
|
} |
86
|
|
|
if ($element instanceof Class_) { |
87
|
|
|
$modifiers = $element->isAbstract() ? ' abstract' : ''; |
88
|
|
|
$modifiers = $element->isFinal() ? ' final' : $modifiers; |
89
|
|
|
if ($modifiers !== '') { |
90
|
|
|
$this->addLine('.. rst-class:: ' . $modifiers)->addLine(); |
91
|
|
|
} |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
$this->callExtensions(self::SECTION_AFTER_TITLE, $element); |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
$this->beginPhpDomain($this->getTypeForClass($element), $element->getName(), false); |
98
|
|
|
$this->addLine(); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
private function getTypeForClass($element) { |
102
|
|
|
switch (get_class($element)) { |
103
|
|
|
case Class_::class: |
104
|
|
|
return 'class'; |
105
|
|
|
case Interface_::class: |
106
|
|
|
return 'interface'; |
107
|
|
|
case Trait_::class: |
108
|
|
|
return 'trait'; |
109
|
|
|
case Function_::class: |
110
|
|
|
return 'function'; |
111
|
|
|
case Method::class: |
112
|
|
|
return 'method'; |
113
|
|
|
default: |
114
|
|
|
return ''; |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
protected function addAfterIntroduction($element) { |
119
|
|
|
$this->callExtensions(self::SECTION_AFTER_INTRODUCTION, $element); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
|
123
|
|
|
protected function addConstants($constants) { |
124
|
|
|
if (count($constants) > 0) { |
125
|
|
|
$this->addH2('Constants'); |
126
|
|
|
foreach ($constants as $constant) { |
127
|
|
|
if ($this->shouldRenderElement($constant)) { |
128
|
|
|
$this->addConstant($constant); |
129
|
|
|
} |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @param Constant $constant |
136
|
|
|
*/ |
137
|
|
|
private function addConstant(Constant $constant) { |
138
|
|
|
$this->beginPhpDomain('const', $constant->getName() . ' = ' . self::escape($constant->getValue())); |
139
|
|
|
$docBlock = $constant->getDocBlock(); |
140
|
|
|
$this->addDocBlockDescription($constant); |
141
|
|
|
if ($docBlock) { |
142
|
|
|
foreach ($docBlock->getTags() as $tag) { |
143
|
|
|
$this->addDocblockTag($tag->getName(), $docBlock); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
$this->endPhpDomain(); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* @param Property[] $properties |
151
|
|
|
*/ |
152
|
|
|
protected function addProperties($properties) { |
153
|
|
|
if (count($properties) > 0) { |
154
|
|
|
$this->addH2('Properties'); |
155
|
|
|
foreach ($properties as $property) { |
156
|
|
|
if ($this->shouldRenderElement($property)) { |
157
|
|
|
$this->addProperty($property); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param Property $property |
165
|
|
|
*/ |
166
|
|
|
private function addProperty(Property $property) { |
167
|
|
|
$modifiers = $property->isStatic() ? '' : ' static' ; |
168
|
|
|
$this->beginPhpDomain('attr', $property->getVisibility() . $modifiers . ' ' . $property->getName()); |
169
|
|
|
$docBlock = $property->getDocBlock(); |
170
|
|
|
$this->addDocBlockDescription($property); |
171
|
|
|
if ($docBlock) { |
172
|
|
|
foreach ($docBlock->getTags() as $tag) { |
173
|
|
|
$this->addDocblockTag($tag->getName(), $docBlock); |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
$this->endPhpDomain(); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @param Interface_|Class_ $element |
181
|
|
|
*/ |
182
|
|
|
protected function addParent($element) { |
183
|
|
|
if ($element instanceof Class_) { |
184
|
|
|
$parent = $element->getParent(); |
185
|
|
|
if ($parent !== null) { |
186
|
|
|
$this->addFieldList('Parent', $parent !== null ? $this->getLink('class', $parent) : ''); |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
if ($element instanceof Interface_) { |
190
|
|
|
$parents = $element->getParents(); |
191
|
|
|
foreach ($parents as $parent) { |
192
|
|
|
$this->addFieldList('Parent', $parent !== null ? $this->getLink('interface', $parent) : ''); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @param Class_|Trait_ $element |
199
|
|
|
*/ |
200
|
|
|
protected function addUsedTraits($element) { |
201
|
|
|
$usedTraits = ''; |
202
|
|
|
foreach ($element->getUsedTraits() as $trait) { |
203
|
|
|
$usedTraits .= $this->getLink('trait', $trait) . ' '; |
204
|
|
|
} |
205
|
|
|
if ($usedTraits !== '') { |
206
|
|
|
$this->addFieldList('Used traits', $usedTraits); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @param $methods |
212
|
|
|
*/ |
213
|
|
|
protected function addMethods($methods) { |
214
|
|
|
if (count($methods) > 0) { |
215
|
|
|
$this->addH2('Methods'); |
216
|
|
|
foreach ($methods as $method) { |
217
|
|
|
$this->addMethod($method); |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
private function addMethod(Method $method) { |
223
|
|
|
if (!$this->shouldRenderElement($method)) { |
224
|
|
|
return; |
225
|
|
|
} |
226
|
|
|
$docBlock = $method->getDocBlock(); |
227
|
|
|
$params = []; |
228
|
|
|
$deprecated = []; |
229
|
|
View Code Duplication |
if ($docBlock !== null) { |
|
|
|
|
230
|
|
|
/** @var Param $param */ |
231
|
|
|
foreach ($docBlock->getTagsByName('param') as $param) { |
232
|
|
|
$params[$param->getVariableName()] = $param; |
|
|
|
|
233
|
|
|
} |
234
|
|
|
$deprecated = $docBlock->getTagsByName('deprecated'); |
235
|
|
|
} |
236
|
|
|
$args = ''; |
237
|
|
|
/** @var Argument $argument */ |
238
|
|
|
foreach ($method->getArguments() as $argument) { |
239
|
|
|
// This will work after https://github.com/phpDocumentor/Reflection/pull/109 is merged |
240
|
|
|
foreach ($argument->getTypes() as $type) { |
241
|
|
|
$args .= self::escape($type) . '|'; |
242
|
|
|
} |
243
|
|
|
$args = substr($args, 0, -1) . ' '; |
244
|
|
|
if($argument->isVariadic()) { |
245
|
|
|
$args .= '...'; |
246
|
|
|
} |
247
|
|
|
if($argument->isByReference()) { |
248
|
|
|
$args .= '&'; |
249
|
|
|
} |
250
|
|
|
$args .= '$' . $argument->getName(); |
251
|
|
|
$default = $argument->getDefault(); |
252
|
|
|
if ($default !== null) { |
253
|
|
|
$default = $default === '' ? '""' : $default; |
254
|
|
|
$args .= '=' . self::escape($default); |
255
|
|
|
} |
256
|
|
|
$args .= ', '; |
257
|
|
|
} |
258
|
|
|
$args = substr($args, 0, -2); |
259
|
|
|
|
260
|
|
|
$modifiers = $method->getVisibility(); |
261
|
|
|
$modifiers .= $method->isAbstract() ? ' abstract' : ''; |
262
|
|
|
$modifiers .= $method->isFinal() ? ' final' : ''; |
263
|
|
|
$modifiers .= $method->isStatic() ? ' static' : ''; |
264
|
|
|
$deprecated = count($deprecated) > 0 ? ' deprecated' : ''; |
265
|
|
|
$this->addLine('.. rst-class:: ' . $modifiers . $deprecated)->addLine(); |
266
|
|
|
$this->indent(); |
267
|
|
|
$this->beginPhpDomain('method', $modifiers . ' ' . $method->getName() . '(' . $args . ')'); |
268
|
|
|
$this->addDocBlockDescription($method); |
269
|
|
|
$this->addLine(); |
270
|
|
|
if (!empty($params)) { |
271
|
|
|
$parameterDetails = ''; |
272
|
|
|
foreach ($method->getArguments() as $argument) { |
273
|
|
|
/** @var Param $param */ |
274
|
|
|
$param = $params[$argument->getName()]; |
275
|
|
|
if ($param !== null) { |
276
|
|
|
$typString = $param->getType(); |
277
|
|
|
// Remove first \ to allow references |
278
|
|
|
if (0 === strpos($typString, '\\')) { |
279
|
|
|
$typString = substr($typString, 1); |
280
|
|
|
} |
281
|
|
|
$paramItem = '* '; |
282
|
|
|
$paramItem .= '**$' . $argument->getName() . '** '; |
283
|
|
|
if ($typString !== null) { |
284
|
|
|
$paramItem .= '(' . self::typesToRst($typString) . ') '; |
285
|
|
|
} |
286
|
|
|
$paramItem .= ' ' . $param->getDescription(); |
287
|
|
|
$parameterDetails .= $paramItem . PHP_EOL; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
$this->addFieldList('Parameters', $parameterDetails); |
291
|
|
|
} |
292
|
|
|
if ($docBlock !== null) { |
293
|
|
|
foreach ($docBlock->getTags() as $tag) { |
294
|
|
|
$this->addDocblockTag($tag->getName(), $docBlock); |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
$this->endPhpDomain('method'); |
298
|
|
|
$this->unindent(); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @param $type string |
303
|
|
|
* @param $fqsen string |
304
|
|
|
* @return string |
305
|
|
|
*/ |
306
|
|
|
public static function getLink($type, $fqsen, $description='') { |
307
|
|
|
if($description !== '') { |
308
|
|
|
return ':php:' . $type . ':`' . RstBuilder::escape($description) . '<' . RstBuilder::escape(substr($fqsen, 1)) . '>`'; |
309
|
|
|
} |
310
|
|
|
return ':php:' . $type . ':`' . RstBuilder::escape(substr($fqsen, 1)) . '`'; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* @param $type string |
315
|
|
|
* @param $name string |
316
|
|
|
* @param $indent bool Should indent after the section started |
317
|
|
|
*/ |
318
|
|
|
public function beginPhpDomain($type, $name, $indent = true) { |
319
|
|
|
// FIXME: Add checks if it is properly ended |
320
|
|
|
$this->addLine('.. php:' . $type . ':: ' . $name)->addLine(); |
321
|
|
|
if ($indent === true) { |
322
|
|
|
$this->indent(); |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* @param string $type |
328
|
|
|
* @return $this |
329
|
|
|
*/ |
330
|
|
|
public function endPhpDomain($type = '') { |
|
|
|
|
331
|
|
|
$this->unindent(); |
332
|
|
|
$this->addLine(); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* @param Class_|Interface_|Trait_|Property|Method|Constant $element |
337
|
|
|
* @return $this |
338
|
|
|
*/ |
339
|
|
|
public function addDocBlockDescription($element) { |
340
|
|
|
if ($element === null) { |
341
|
|
|
return $this; |
342
|
|
|
} |
343
|
|
|
$docBlock = $element->getDocBlock(); |
344
|
|
|
$this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element); |
345
|
|
|
if ($docBlock !== null && $docBlock->getSummary() !== '') { |
346
|
|
|
$this->addLine('.. rst-class:: phpdoc-description')->addLine(); |
347
|
|
|
$this->indent(); |
348
|
|
|
$this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getSummary()))->addLine(); |
349
|
|
|
if ((string)$docBlock->getDescription() !== '') { |
350
|
|
|
$this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getDescription()))->addLine(); |
351
|
|
|
} |
352
|
|
|
$this->unindent(); |
353
|
|
|
} |
354
|
|
|
$this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element); |
355
|
|
|
return $this; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* @param string $tagName Name of the tag to parse |
360
|
|
|
* @param DocBlock $docBlock |
361
|
|
|
*/ |
362
|
|
|
protected function addDocblockTag($tagName, DocBlock $docBlock) { |
363
|
|
|
$tags = $docBlock->getTagsByName($tagName); |
364
|
|
|
switch ($tagName) { |
365
|
|
View Code Duplication |
case 'return': |
|
|
|
|
366
|
|
|
if (count($tags) === 0) continue; |
367
|
|
|
/** @var Return_ $return */ |
368
|
|
|
$return = $tags[0]; |
369
|
|
|
$this->addMultiline(':Returns: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
370
|
|
|
break; |
371
|
|
|
case 'var': |
372
|
|
|
if (count($tags) === 0) continue; |
373
|
|
|
/** @var DocBlock\Tags\Var_ $return */ |
374
|
|
|
$return = $tags[0]; |
375
|
|
|
$this->addMultiline(':Type: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
376
|
|
|
break; |
377
|
|
View Code Duplication |
case 'throws': |
|
|
|
|
378
|
|
|
if (count($tags) === 0) continue; |
379
|
|
|
/** @var Throws $tag */ |
380
|
|
|
foreach ($tags as $tag) { |
381
|
|
|
$this->addMultiline(':Throws: ' . self::typesToRst($tag->getType()) . ' ' . RstBuilder::escape($tag->getDescription()), true); |
|
|
|
|
382
|
|
|
} |
383
|
|
|
break; |
384
|
|
View Code Duplication |
case 'since': |
|
|
|
|
385
|
|
|
if (count($tags) === 0) continue; |
386
|
|
|
/** @var Since $return */ |
387
|
|
|
$return = $tags[0]; |
388
|
|
|
$this->addMultiline(':Since: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true); |
389
|
|
|
break; |
390
|
|
View Code Duplication |
case 'deprecated': |
|
|
|
|
391
|
|
|
if (count($tags) === 0) continue; |
392
|
|
|
/** @var Deprecated $return */ |
393
|
|
|
$return = $tags[0]; |
394
|
|
|
$this->addMultiline(':Deprecated: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true); |
395
|
|
|
break; |
396
|
|
|
case 'see': |
397
|
|
|
if (count($tags) === 0) continue; |
398
|
|
|
/** @var See $return */ |
399
|
|
|
$return = $tags[0]; |
400
|
|
|
$this->addMultiline(':See: ' . self::typesToRst($return->getReference()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
401
|
|
|
break; |
402
|
|
View Code Duplication |
case 'license': |
|
|
|
|
403
|
|
|
if (count($tags) === 0) continue; |
404
|
|
|
/** @var DocBlock\Tags\BaseTag $return */ |
405
|
|
|
$return = $tags[0]; |
406
|
|
|
$this->addMultiline(':License: ' . RstBuilder::escape($return->getDescription()), true); |
407
|
|
|
break; |
408
|
|
|
case 'param': |
409
|
|
|
// param handling is done by subclasses since it is more that docbook parsing |
410
|
|
|
break; |
411
|
|
|
default: |
412
|
|
|
//echo 'Tag handling not defined for: ' . $tag . PHP_EOL; |
413
|
|
|
break; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* @param string $typesString |
420
|
|
|
* @return bool|string |
421
|
|
|
*/ |
422
|
|
|
public static function typesToRst($typesString) { |
423
|
|
|
// http://docs.phpdoc.org/guides/types.html |
424
|
|
|
$whitelist = [ |
425
|
|
|
'string', 'int', 'integer', 'float', 'bool', 'boolean', 'array', 'resource', 'null', 'callable', |
426
|
|
|
'mixed', 'void', 'object', 'false', 'true', 'self', 'static', '$this' |
427
|
|
|
]; |
428
|
|
|
$types = explode('|', $typesString); |
429
|
|
|
$result = ''; |
430
|
|
|
/** @var string $type */ |
431
|
|
|
foreach ($types as $typeFull) { |
432
|
|
|
$type = str_replace('[]', '', $typeFull); |
433
|
|
|
if (in_array($type, $whitelist, true)) { |
434
|
|
|
$result .= $typeFull . ' | '; |
435
|
|
|
continue; |
436
|
|
|
} |
437
|
|
|
if (0 === strpos($type, '\\')) |
438
|
|
|
$type = substr($type, 1); |
439
|
|
|
$result .= ':any:`' . RstBuilder::escape($typeFull) . ' <' . RstBuilder::escape($type) . '>` | '; |
440
|
|
|
} |
441
|
|
|
return substr($result, 0, -3); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* @param Element $element |
446
|
|
|
* @return bool |
447
|
|
|
*/ |
448
|
|
|
public function shouldRenderElement(Element $element) { |
449
|
|
|
/** @var Extension $extension */ |
450
|
|
|
foreach ($this->extensions as $extension) { |
451
|
|
|
if ($extension->shouldRenderElement($element) === false) { |
452
|
|
|
return false; |
453
|
|
|
} |
454
|
|
|
} |
455
|
|
|
return true; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
|
459
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.