This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace BestIt\Sniffs\DocTags; |
||
6 | |||
7 | use BestIt\CodeSniffer\Helper\DocTagHelper; |
||
8 | use BestIt\Sniffs\AbstractSniff; |
||
9 | use BestIt\Sniffs\DocPosProviderTrait; |
||
10 | use Closure; |
||
11 | use function array_filter; |
||
12 | use function array_key_exists; |
||
13 | use function array_map; |
||
14 | use function array_walk; |
||
15 | use function count; |
||
16 | use function in_array; |
||
17 | use function is_callable; |
||
18 | use function method_exists; |
||
19 | use function sprintf; |
||
20 | use function substr; |
||
21 | use function ucfirst; |
||
22 | |||
23 | /** |
||
24 | * Abstract sniff for the the required tags of a php structure. |
||
25 | * |
||
26 | * @author blange <[email protected]> |
||
27 | * @package BestIt\Sniffs\DocTags |
||
28 | */ |
||
29 | abstract class AbstractRequiredTagsSniff extends AbstractSniff |
||
30 | { |
||
31 | use DocPosProviderTrait; |
||
32 | |||
33 | /** |
||
34 | * You MUST provide only the maximum amount of required tags. For example, only one return per method is allowed. The error is registered for every tag specifically. |
||
35 | */ |
||
36 | public const CODE_TAG_OCCURRENCE_MAX_PREFIX = 'TagOccurrenceMax'; |
||
37 | |||
38 | /** |
||
39 | * You MUST provide the required tags. The error is registered for every tag specifically. |
||
40 | */ |
||
41 | public const CODE_TAG_OCCURRENCE_MIN_PREFIX = 'TagOccurrenceMin'; |
||
42 | |||
43 | /** |
||
44 | * Message that comment tag must appear maximum x times. |
||
45 | */ |
||
46 | private const MESSAGE_TAG_OCCURRENCE_MAX = 'The comment tag "%s" must appear maximum %d times. Found %d times.'; |
||
47 | |||
48 | /** |
||
49 | * Message that comment tag must appear minimum x times. |
||
50 | */ |
||
51 | private const MESSAGE_TAG_OCCURRENCE_MIN = 'The comment tag "%s" must appear minimum %d times. Found %d times.'; |
||
52 | |||
53 | /** |
||
54 | * Caches the processed tag rules. |
||
55 | * |
||
56 | * @var array|null |
||
57 | */ |
||
58 | private ?array $processedTagRules = null; |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
59 | |||
60 | /** |
||
61 | * The possible tags of this php structure. |
||
62 | * |
||
63 | * @var array|null Tag tokens. |
||
64 | */ |
||
65 | private ?array $tags = null; |
||
66 | |||
67 | /** |
||
68 | * Returns true if the requirements for this sniff are met. |
||
69 | * |
||
70 | * @return bool Are the requirements met and the sniff should proceed? |
||
71 | */ |
||
72 | protected function areRequirementsMet(): bool |
||
73 | { |
||
74 | return (bool) $this->getDocCommentPos(); |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * Loads the rules with amax rule and checks if there are more tags than allowed. |
||
79 | * |
||
80 | * @return void |
||
81 | */ |
||
82 | private function checkAndRegisterTagMaximumCounts(): void |
||
83 | { |
||
84 | $allTags = $this->getAllTags(); |
||
85 | $checkedTags = []; |
||
86 | $tagRules = $this->getRulesWithRequirement('max'); |
||
87 | |||
88 | array_walk( |
||
89 | $allTags, |
||
90 | // TODO: Removed Duplicates |
||
91 | function (array $tag, int $tagPos) use (&$checkedTags, $tagRules): void { |
||
92 | $tagContent = substr($tag['content'], 1); |
||
93 | |||
94 | if (!in_array($tagContent, $checkedTags)) { |
||
95 | $checkedTags[] = $tagContent; |
||
96 | |||
97 | $tagCount = count($this->findTokensForTag($tagContent)); |
||
98 | $maxCount = @$tagRules[$tagContent]['max'] ?? 0; |
||
99 | |||
100 | if ($maxCount && ($tagCount > $maxCount)) { |
||
101 | $this->file->addError( |
||
102 | self::MESSAGE_TAG_OCCURRENCE_MAX, |
||
103 | $tagPos, |
||
104 | // We use an error code containing the tag name because we can't configure this rules from |
||
105 | // the outside and need specific code to exclude the rule for this special tag. |
||
106 | static::CODE_TAG_OCCURRENCE_MAX_PREFIX . ucfirst($tagContent), |
||
107 | [ |
||
108 | $tagContent, |
||
109 | $maxCount, |
||
110 | $tagCount |
||
111 | ] |
||
112 | ); |
||
113 | } |
||
114 | } |
||
115 | } |
||
116 | ); |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Checks if the tag occurrences reaches their minimum counts. |
||
121 | * |
||
122 | * @return void |
||
123 | */ |
||
124 | private function checkAndRegisterTagMinimumCounts(): void |
||
125 | { |
||
126 | $checkedRule = 'min'; |
||
127 | $rulesWithReq = $this->getRulesWithRequirement($checkedRule); |
||
128 | |||
129 | array_walk( |
||
130 | $rulesWithReq, |
||
131 | function (array $tagRule, string $tag) use ($checkedRule): void { |
||
132 | $minCount = $tagRule[$checkedRule]; |
||
133 | $tagCount = count($this->findTokensForTag($tag)); |
||
134 | |||
135 | if ($minCount > $tagCount) { |
||
136 | $fixCallback = $this->hasFixCallback($checkedRule, $tag); |
||
137 | $method = $fixCallback ? 'addFixableError' : 'addError'; |
||
138 | $fixable = $this->file->{$method}( |
||
139 | self::MESSAGE_TAG_OCCURRENCE_MIN, |
||
140 | $this->getDocCommentPos(), |
||
141 | // We use an error code containing the tag name because we can't configure this rules from the |
||
142 | // outside and need specific code to exclude the rule for this special tag. |
||
143 | static::CODE_TAG_OCCURRENCE_MIN_PREFIX . ucfirst($tag), |
||
144 | [ |
||
145 | $tag, |
||
146 | $minCount, |
||
147 | $tagCount |
||
148 | ] |
||
149 | ); |
||
150 | |||
151 | if ($fixable && $fixCallback) { |
||
152 | $this->{$fixCallback}($this->getDocCommentPos(), $minCount, $tagCount, $tag); |
||
153 | } |
||
154 | } |
||
155 | } |
||
156 | ); |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Returns only the tokens with the given tag name. |
||
161 | * |
||
162 | * @param string $tagName |
||
163 | * |
||
164 | * @return array |
||
165 | */ |
||
166 | private function findTokensForTag(string $tagName): array |
||
167 | { |
||
168 | $allTags = $this->getAllTags(); |
||
169 | |||
170 | return array_filter($allTags, function (array $tag) use ($tagName): bool { |
||
171 | return substr($tag['content'], 1) === $tagName; |
||
172 | }); |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Returns all tag tokens for this doc block. |
||
177 | * |
||
178 | * @return array |
||
179 | */ |
||
180 | private function getAllTags(): array |
||
181 | { |
||
182 | if ($this->tags === null) { |
||
183 | $this->tags = $this->loadAllTags(); |
||
184 | } |
||
185 | |||
186 | return $this->tags; |
||
187 | } |
||
188 | |||
189 | /** |
||
190 | * Returns the rules with their resolved callbacks. |
||
191 | * |
||
192 | * @return array |
||
193 | */ |
||
194 | private function getProcessedTagRules(): array |
||
195 | { |
||
196 | if ($this->processedTagRules === null) { |
||
197 | $this->processedTagRules = $this->processTagRules(); |
||
198 | } |
||
199 | |||
200 | return $this->processedTagRules; |
||
201 | } |
||
202 | |||
203 | /** |
||
204 | * Returns the rules with the given requirement. |
||
205 | * |
||
206 | * @param string $requiredRule |
||
207 | * |
||
208 | * @return array |
||
209 | */ |
||
210 | private function getRulesWithRequirement(string $requiredRule): array |
||
211 | { |
||
212 | $processedTagRules = $this->getProcessedTagRules(); |
||
213 | |||
214 | $processedTagRules = array_filter($processedTagRules, function (array $tagRule) use ($requiredRule): bool { |
||
215 | return array_key_exists($requiredRule, $tagRule); |
||
216 | }); |
||
217 | return $processedTagRules; |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Returns the required tag data. |
||
222 | * |
||
223 | * The order in which they appear in this array os the order for tags needed. |
||
224 | * |
||
225 | * @return array List of tag metadata |
||
226 | */ |
||
227 | abstract protected function getTagRules(): array; |
||
228 | |||
229 | /** |
||
230 | * Returns true if there is a callback for the given rule and tag. |
||
231 | * |
||
232 | * @param string $rule |
||
233 | * @param string $tag |
||
234 | * |
||
235 | * @return bool|string |
||
236 | */ |
||
237 | private function hasFixCallback(string $rule, string $tag) |
||
238 | { |
||
239 | return method_exists($this, $method = sprintf('fix%s%s', ucfirst($rule), ucfirst($tag))) |
||
240 | ? $method |
||
241 | : false; |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * Loads all tags of the structures doc block. |
||
246 | * |
||
247 | * @return array |
||
248 | */ |
||
249 | private function loadAllTags(): array |
||
250 | { |
||
251 | return (new DocTagHelper( |
||
252 | $this->file, |
||
253 | $this->getDocCommentPos(), |
||
254 | $this->tokens |
||
255 | ) |
||
256 | )->getTagTokens(); |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * Resolves the callbacks in the tag rules. |
||
261 | * |
||
262 | * @return array |
||
263 | */ |
||
264 | private function processTagRules(): array |
||
265 | { |
||
266 | $processedTagRules = $this->getTagRules(); |
||
267 | |||
268 | array_walk($processedTagRules, function (&$tagRule) { |
||
269 | $tagRule = array_map(function ($valueOrCallback) { |
||
270 | return is_callable($valueOrCallback, true) |
||
271 | ? Closure::fromCallable($valueOrCallback)->call($this) |
||
272 | : $valueOrCallback; |
||
273 | }, $tagRule); |
||
274 | }); |
||
275 | |||
276 | return $processedTagRules; |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Processes a found registered token. |
||
281 | * |
||
282 | * @return void |
||
283 | */ |
||
284 | protected function processToken(): void |
||
285 | { |
||
286 | $this->checkAndRegisterTagMaximumCounts(); |
||
287 | $this->checkAndRegisterTagMinimumCounts(); |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Resets the cached data. |
||
292 | * |
||
293 | * @return void |
||
294 | */ |
||
295 | protected function tearDown(): void |
||
296 | { |
||
297 | parent::tearDown(); |
||
298 | |||
299 | $this->resetDocCommentPos(); |
||
300 | |||
301 | $this->processedTagRules = null; |
||
302 | $this->tags = null; |
||
303 | } |
||
304 | } |
||
305 |