Total Complexity | 64 |
Total Lines | 364 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Language often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Language, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
48 | class Language extends Mode |
||
49 | { |
||
50 | /** @var string[] */ |
||
51 | private static $COMMON_KEYWORDS = array('of', 'and', 'for', 'in', 'not', 'or', 'if', 'then'); |
||
52 | |||
53 | /** @var string */ |
||
54 | public $name; |
||
55 | |||
56 | /** @var Mode|null */ |
||
57 | private $mode = null; |
||
58 | |||
59 | /** |
||
60 | * @param string $lang |
||
61 | * @param string $filePath |
||
62 | * |
||
63 | * @throws \InvalidArgumentException when the given $filePath is inaccessible |
||
64 | */ |
||
65 | public function __construct($lang, $filePath) |
||
66 | { |
||
67 | $this->name = $lang; |
||
68 | |||
69 | // We're loading the JSON definition file as an \stdClass object instead of an associative array. This is being |
||
70 | // done to take advantage of objects being pass by reference automatically in PHP whereas arrays are pass by |
||
71 | // value. |
||
72 | $json = file_get_contents($filePath); |
||
73 | |||
74 | if ($json === false) { |
||
75 | throw new \InvalidArgumentException("Language file inaccessible: $filePath"); |
||
76 | } |
||
77 | |||
78 | $this->mode = json_decode($json); |
||
79 | } |
||
80 | |||
81 | /** |
||
82 | * @param string $name |
||
83 | * |
||
84 | * @return bool|Mode|null |
||
85 | */ |
||
86 | public function __get($name) |
||
87 | { |
||
88 | if ($name === 'mode') { |
||
89 | @trigger_error('The "mode" property will be removed in highlight.php 10.x', E_USER_DEPRECATED); |
||
90 | |||
91 | return $this->mode; |
||
92 | } |
||
93 | |||
94 | if ($name === 'caseInsensitive') { |
||
95 | @trigger_error('Due to compatibility requirements with highlight.js, use "case_insensitive" instead.', E_USER_DEPRECATED); |
||
96 | |||
97 | if (isset($this->mode->case_insensitive)) { |
||
98 | return $this->mode->case_insensitive; |
||
99 | } |
||
100 | |||
101 | return false; |
||
102 | } |
||
103 | |||
104 | if (isset($this->mode->{$name})) { |
||
105 | return $this->mode->{$name}; |
||
106 | } |
||
107 | |||
108 | return null; |
||
109 | } |
||
110 | |||
111 | /** |
||
112 | * @param string $value |
||
113 | * @param bool $global |
||
114 | * |
||
115 | * @return RegEx |
||
116 | */ |
||
117 | private function langRe($value, $global = false) |
||
118 | { |
||
119 | return RegExUtils::langRe($value, $global, $this->case_insensitive); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * Performs a shallow merge of multiple objects into one. |
||
124 | * |
||
125 | * @param Mode $params the objects to merge |
||
126 | * @param array<string, mixed> ...$_ |
||
127 | * |
||
128 | * @return Mode |
||
129 | */ |
||
130 | private function inherit($params, $_ = array()) |
||
131 | { |
||
132 | /** @var Mode $result */ |
||
133 | $result = new \stdClass(); |
||
134 | $objects = func_get_args(); |
||
135 | $parent = array_shift($objects); |
||
136 | |||
137 | foreach ($parent as $key => $value) { |
||
138 | $result->{$key} = $value; |
||
139 | } |
||
140 | |||
141 | foreach ($objects as $object) { |
||
142 | foreach ($object as $key => $value) { |
||
143 | $result->{$key} = $value; |
||
144 | } |
||
145 | } |
||
146 | |||
147 | return $result; |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * @param Mode|null $mode |
||
152 | * |
||
153 | * @return bool |
||
154 | */ |
||
155 | private function dependencyOnParent($mode) |
||
156 | { |
||
157 | if (!$mode) { |
||
158 | return false; |
||
159 | } |
||
160 | |||
161 | if (isset($mode->endsWithParent) && $mode->endsWithParent) { |
||
162 | return $mode->endsWithParent; |
||
163 | } |
||
164 | |||
165 | return $this->dependencyOnParent(isset($mode->starts) ? $mode->starts : null); |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * @param Mode $mode |
||
170 | * |
||
171 | * @return array<int, \stdClass|Mode> |
||
172 | */ |
||
173 | private function expandOrCloneMode($mode) |
||
174 | { |
||
175 | if ($mode->variants && !$mode->cachedVariants) { |
||
176 | $mode->cachedVariants = array(); |
||
177 | |||
178 | foreach ($mode->variants as $variant) { |
||
179 | $mode->cachedVariants[] = $this->inherit($mode, array('variants' => null), $variant); |
||
180 | } |
||
181 | } |
||
182 | |||
183 | // EXPAND |
||
184 | // if we have variants then essentially "replace" the mode with the variants |
||
185 | // this happens in compileMode, where this function is called from |
||
186 | if ($mode->cachedVariants) { |
||
187 | return $mode->cachedVariants; |
||
188 | } |
||
189 | |||
190 | // CLONE |
||
191 | // if we have dependencies on parents then we need a unique |
||
192 | // instance of ourselves, so we can be reused with many |
||
193 | // different parents without issue |
||
194 | if ($this->dependencyOnParent($mode)) { |
||
195 | return array($this->inherit($mode, array( |
||
196 | 'starts' => $mode->starts ? $this->inherit($mode->starts) : null, |
||
197 | ))); |
||
198 | } |
||
199 | |||
200 | // highlight.php does not have a concept freezing our Modes |
||
201 | |||
202 | // no special dependency issues, just return ourselves |
||
203 | return array($mode); |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * @param Mode $mode |
||
208 | * @param Mode|null $parent |
||
209 | * |
||
210 | * @return void |
||
211 | */ |
||
212 | private function compileMode($mode, $parent = null) |
||
213 | { |
||
214 | Mode::_normalize($mode); |
||
215 | |||
216 | if ($mode->compiled) { |
||
217 | return; |
||
218 | } |
||
219 | |||
220 | $mode->compiled = true; |
||
221 | $mode->keywords = $mode->keywords ? $mode->keywords : $mode->beginKeywords; |
||
222 | |||
223 | if ($mode->keywords) { |
||
224 | $mode->keywords = $this->compileKeywords($mode->keywords, (bool) $this->case_insensitive); |
||
225 | } |
||
226 | |||
227 | $mode->lexemesRe = $this->langRe($mode->lexemes ? $mode->lexemes : "\w+", true); |
||
228 | |||
229 | if ($parent) { |
||
230 | if ($mode->beginKeywords) { |
||
231 | $mode->begin = "\\b(" . implode("|", explode(" ", $mode->beginKeywords)) . ")\\b"; |
||
232 | } |
||
233 | |||
234 | if (!$mode->begin) { |
||
235 | $mode->begin = "\B|\b"; |
||
236 | } |
||
237 | |||
238 | $mode->beginRe = $this->langRe($mode->begin); |
||
239 | |||
240 | if ($mode->endSameAsBegin) { |
||
241 | $mode->end = $mode->begin; |
||
242 | } |
||
243 | |||
244 | if (!$mode->end && !$mode->endsWithParent) { |
||
245 | $mode->end = "\B|\b"; |
||
246 | } |
||
247 | |||
248 | if ($mode->end) { |
||
249 | $mode->endRe = $this->langRe($mode->end); |
||
250 | } |
||
251 | |||
252 | $mode->terminator_end = $mode->end; |
||
253 | |||
254 | if ($mode->endsWithParent && $parent->terminator_end) { |
||
255 | $mode->terminator_end .= ($mode->end ? "|" : "") . $parent->terminator_end; |
||
256 | } |
||
257 | } |
||
258 | |||
259 | if ($mode->illegal) { |
||
260 | $mode->illegalRe = $this->langRe($mode->illegal); |
||
261 | } |
||
262 | |||
263 | if ($mode->relevance === null) { |
||
264 | $mode->relevance = 1; |
||
265 | } |
||
266 | |||
267 | if (!$mode->contains) { |
||
268 | $mode->contains = array(); |
||
269 | } |
||
270 | |||
271 | /** @var Mode[] $expandedContains */ |
||
272 | $expandedContains = array(); |
||
273 | foreach ($mode->contains as &$c) { |
||
274 | if ($c instanceof \stdClass) { |
||
275 | Mode::_normalize($c); |
||
276 | } |
||
277 | |||
278 | $expandedContains = array_merge($expandedContains, $this->expandOrCloneMode( |
||
279 | $c === 'self' ? $mode : $c |
||
280 | )); |
||
281 | } |
||
282 | $mode->contains = $expandedContains; |
||
283 | |||
284 | /** @var Mode $contain */ |
||
285 | foreach ($mode->contains as $contain) { |
||
286 | $this->compileMode($contain, $mode); |
||
287 | } |
||
288 | |||
289 | if ($mode->starts) { |
||
290 | $this->compileMode($mode->starts, $parent); |
||
291 | } |
||
292 | |||
293 | $terminators = new Terminators($this->case_insensitive); |
||
294 | $mode->terminators = $terminators->_buildModeRegex($mode); |
||
295 | |||
296 | Mode::_handleDeprecations($mode); |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * @param array<string, string>|string $rawKeywords |
||
301 | * @param bool $caseSensitive |
||
302 | * |
||
303 | * @return array<string, array<int, string|int>> |
||
304 | */ |
||
305 | private function compileKeywords($rawKeywords, $caseSensitive) |
||
306 | { |
||
307 | /** @var array<string, array<int, string|int>> $compiledKeywords */ |
||
308 | $compiledKeywords = array(); |
||
309 | |||
310 | if (is_string($rawKeywords)) { |
||
311 | $this->splitAndCompile("keyword", $rawKeywords, $compiledKeywords, $caseSensitive); |
||
312 | } else { |
||
313 | foreach ($rawKeywords as $className => $rawKeyword) { |
||
314 | $this->splitAndCompile($className, $rawKeyword, $compiledKeywords, $caseSensitive); |
||
315 | } |
||
316 | } |
||
317 | |||
318 | return $compiledKeywords; |
||
319 | } |
||
320 | |||
321 | /** |
||
322 | * @param string $className |
||
323 | * @param string $str |
||
324 | * @param array<string, array<int, string|int>> $compiledKeywords |
||
325 | * @param bool $caseSensitive |
||
326 | * |
||
327 | * @return void |
||
328 | */ |
||
329 | private function splitAndCompile($className, $str, array &$compiledKeywords, $caseSensitive) |
||
330 | { |
||
331 | if ($caseSensitive) { |
||
332 | $str = strtolower($str); |
||
333 | } |
||
334 | |||
335 | $keywords = explode(' ', $str); |
||
336 | |||
337 | foreach ($keywords as $keyword) { |
||
338 | $pair = explode('|', $keyword); |
||
339 | $providedScore = isset($pair[1]) ? $pair[1] : null; |
||
340 | $compiledKeywords[$pair[0]] = array($className, $this->scoreForKeyword($pair[0], $providedScore)); |
||
341 | } |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * @param string $keyword |
||
346 | * @param string $providedScore |
||
347 | * |
||
348 | * @return int |
||
349 | */ |
||
350 | private function scoreForKeyword($keyword, $providedScore) |
||
351 | { |
||
352 | if ($providedScore) { |
||
353 | return (int) $providedScore; |
||
354 | } |
||
355 | |||
356 | return $this->commonKeyword($keyword) ? 0 : 1; |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * @param string $word |
||
361 | * |
||
362 | * @return bool |
||
363 | */ |
||
364 | private function commonKeyword($word) |
||
365 | { |
||
366 | return in_array(strtolower($word), self::$COMMON_KEYWORDS); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Compile the Language definition. |
||
371 | * |
||
372 | * @param bool $safeMode |
||
373 | * |
||
374 | * @since 9.17.1.0 The 'safeMode' parameter was added. |
||
375 | * |
||
376 | * @return void |
||
377 | */ |
||
378 | public function compile($safeMode) |
||
379 | { |
||
380 | if ($this->compiled) { |
||
381 | return; |
||
382 | } |
||
383 | |||
384 | $jr = new JsonRef(); |
||
385 | $jr->decodeRef($this->mode); |
||
386 | |||
387 | // self is not valid at the top-level |
||
388 | if (isset($this->mode->contains) && !in_array("self", $this->mode->contains)) { |
||
389 | if (!$safeMode) { |
||
390 | throw new \LogicException("`self` is not supported at the top-level of a language."); |
||
391 | } |
||
392 | $this->mode->contains = array_filter($this->mode->contains, function ($mode) { |
||
393 | return $mode !== "self"; |
||
394 | }); |
||
395 | } |
||
396 | |||
397 | $this->compileMode($this->mode); |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * @todo Remove in highlight.php 10.x |
||
402 | * |
||
403 | * @deprecated 9.16.0 This method should never have been exposed publicly as part of the API. |
||
404 | * |
||
405 | * @param \stdClass|null $e |
||
406 | * |
||
407 | * @return void |
||
408 | */ |
||
409 | public function complete(&$e) |
||
412 | } |
||
413 | } |
||
414 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.