Language::compileMode()   F
last analyzed

Complexity

Conditions 23
Paths > 20000

Size

Total Lines 85
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 43
nc 37249
nop 2
dl 0
loc 85
rs 0
c 0
b 0
f 0

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
/* Copyright (c)
4
 * - 2006-2013, Ivan Sagalaev (maniacsoftwaremaniacs.org), highlight.js
5
 *              (original author)
6
 * - 2013-2019, Geert Bergman (geertscrivo.nl), highlight.php
7
 * - 2014       Daniel Lynge, highlight.php (contributor)
8
 *
9
 * Redistribution and use in source and binary forms, with or without
10
 * modification, are permitted provided that the following conditions are met:
11
 *
12
 * 1. Redistributions of source code must retain the above copyright notice,
13
 *    this list of conditions and the following disclaimer.
14
 * 2. Redistributions in binary form must reproduce the above copyright notice,
15
 *    this list of conditions and the following disclaimer in the documentation
16
 *    and/or other materials provided with the distribution.
17
 * 3. Neither the name of "highlight.js", "highlight.php", nor the names of its
18
 *    contributors may be used to endorse or promote products derived from this
19
 *    software without specific prior written permission.
20
 *
21
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
 * POSSIBILITY OF SUCH DAMAGE.
32
 */
33
34
namespace Highlight;
35
36
/**
37
 * @todo In highlight.php 10.x, replace the @final attribute with the `final` keyword.
38
 *
39
 * @final
40
 *
41
 * @internal
42
 *
43
 * // Backward compatibility properties
44
 *
45
 * @property Mode $mode (DEPRECATED) All properties traditionally inside of $mode are now available directly from this class.
46
 * @property bool $caseInsensitive (DEPRECATED) Due to compatibility requirements with highlight.js, use `case_insensitive` instead.
47
 */
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mode->cachedVariants of type array<integer,Highlight\Mode> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
Bug Best Practice introduced by
The expression $mode->variants of type Highlight\Mode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mode->cachedVariants of type array<integer,Highlight\Mode> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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)
410
    {
411
        Mode::_normalize($e);
412
    }
413
}
414