RuleSetFactory::parseSingleRuleNode()   F
last analyzed

Complexity

Conditions 17
Paths 800

Size

Total Lines 66

Duplication

Lines 11
Ratio 16.67 %

Code Coverage

Tests 38
CRAP Score 17.0048

Importance

Changes 0
Metric Value
cc 17
nc 800
nop 2
dl 11
loc 66
ccs 38
cts 39
cp 0.9744
crap 17.0048
rs 1.3277
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
 * This file is part of PHP Mess Detector.
4
 *
5
 * Copyright (c) Manuel Pichler <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * Licensed under BSD License
9
 * For full copyright and license information, please see the LICENSE file.
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @author Manuel Pichler <[email protected]>
13
 * @copyright Manuel Pichler. All rights reserved.
14
 * @license https://opensource.org/licenses/bsd-license.php BSD License
15
 * @link http://phpmd.org/
16
 */
17
18
namespace PHPMD;
19
20
use RuntimeException;
21
22
/**
23
 * This factory class is used to create the {@link \PHPMD\RuleSet} instance
24
 * that PHPMD will use to analyze the source code.
25
 */
26
class RuleSetFactory
27
{
28
    /**
29
     * Is the strict mode active?
30
     *
31
     * @var boolean
32
     * @since 1.2.0
33
     */
34
    private $strict = false;
35
36
    /**
37
     * The data directory set within the class constructor.
38
     *
39
     * @var string
40
     */
41
    private $location;
42
43
    /**
44
     * The minimum priority for rules to load.
45
     *
46
     * @var integer
47
     */
48
    private $minimumPriority = Rule::LOWEST_PRIORITY;
49
50
    /**
51
     * The maximum priority for rules to load.
52
     *
53
     * @var integer
54
     */
55
    private $maximumPriority = Rule::HIGHEST_PRIORITY;
56
57
    /**
58
     * Constructs a new default rule-set factory instance.
59 48
     */
60
    public function __construct()
61
    {
62 48
        $this->location = __DIR__ . '/../../resources';
63 48
    }
64 48
65
    /**
66
     * Activates the strict mode for all rule sets.
67
     *
68
     * @return void
69
     * @since 1.2.0
70
     */
71
    public function setStrict()
72
    {
73
        $this->strict = true;
74
    }
75 1
76
    /**
77 1
     * Sets the minimum priority that a rule must have.
78 1
     *
79
     * @param integer $minimumPriority The minimum priority value.
80
     * @return void
81
     */
82
    public function setMinimumPriority($minimumPriority)
83
    {
84
        $this->minimumPriority = $minimumPriority;
85
    }
86 12
87
    /**
88 12
     * Sets the maximum priority that a rule must have.
89 12
     *
90
     * @param integer $maximumPriority The maximum priority value.
91
     * @return void
92
     */
93
    public function setMaximumPriority($maximumPriority)
94
    {
95
        $this->maximumPriority = $maximumPriority;
96
    }
97 12
98
    /**
99 12
     * Creates an array of rule-set instances for the given argument.
100 12
     *
101
     * @param string $ruleSetFileNames Comma-separated string of rule-set filenames or identifier.
102
     * @return \PHPMD\RuleSet[]
103
     */
104
    public function createRuleSets($ruleSetFileNames)
105
    {
106
        $ruleSets = array();
107
108 40
        $ruleSetFileName = strtok($ruleSetFileNames, ',');
109
        while ($ruleSetFileName !== false) {
110 40
            $ruleSets[] = $this->createSingleRuleSet($ruleSetFileName);
111
112 40
            $ruleSetFileName = strtok(',');
113 40
        }
114 40
115
        return $ruleSets;
116 36
    }
117
118 36
    /**
119
     * Creates a single rule-set instance for the given filename or identifier.
120
     *
121
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
122
     * @return \PHPMD\RuleSet
123
     */
124
    public function createSingleRuleSet($ruleSetOrFileName)
125
    {
126
        $fileName = $this->createRuleSetFileName($ruleSetOrFileName);
127 46
128
        return $this->parseRuleSetNode($fileName);
129 46
    }
130 45
131
    /**
132
     * Lists available rule-set identifiers.
133
     *
134
     * @return string[]
135
     */
136
    public function listAvailableRuleSets()
137
    {
138 4
        return array_merge(
139
            self::listRuleSetsInDirectory($this->location . '/rulesets/'),
140 4
            self::listRuleSetsInDirectory(getcwd() . '/rulesets/')
141 4
        );
142 4
    }
143
144
    /**
145
     * This method creates the filename for a rule-set identifier or it returns
146
     * the input when it is already a filename.
147
     *
148
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
149
     * @return string Path to rule set file name
150
     * @throws RuleSetNotFoundException Thrown if no readable file found
151
     */
152
    private function createRuleSetFileName($ruleSetOrFileName)
153
    {
154 48
        foreach ($this->filePaths($ruleSetOrFileName) as $filePath) {
155
            if ($this->isReadableFile($filePath)) {
156 48
                return $filePath;
157 48
            }
158 47
        }
159
160
        throw new RuleSetNotFoundException($ruleSetOrFileName);
161
    }
162 2
163
    /**
164
     * Lists available rule-set identifiers in given directory.
165
     *
166
     * @param string $directory The directory to scan for rule-sets.
167
     * @return string[]
168
     */
169
    private static function listRuleSetsInDirectory($directory)
170
    {
171 4
        $ruleSets = array();
172
        if (is_dir($directory)) {
173 4
            foreach (scandir($directory) as $file) {
174 4
                $matches = array();
175 4
                if (is_file($directory . $file) && preg_match('/^(.*)\.xml$/', $file, $matches)) {
176 4
                    $ruleSets[] = $matches[1];
177 4
                }
178 4
            }
179
        }
180
181
        return $ruleSets;
182 4
    }
183
184
    /**
185
     * This method parses the rule-set definition in the given file.
186
     *
187
     * @param string $fileName
188
     * @return \PHPMD\RuleSet
189
     * @throws RuntimeException When loading the XML file fails.
190
     */
191 45
    private function parseRuleSetNode($fileName)
192
    {
193
        // Hide error messages
194 45
        $libxml = libxml_use_internal_errors(true);
195
196 45
        $xml = simplexml_load_string(file_get_contents($fileName));
197 45 View Code Duplication
        if ($xml === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
198
            // Reset error handling to previous setting
199 1
            libxml_use_internal_errors($libxml);
200
201 1
            throw new RuntimeException(trim(libxml_get_last_error()->message));
202
        }
203
204 44
        $ruleSet = new RuleSet();
205 44
        $ruleSet->setFileName($fileName);
206 44
        $ruleSet->setName((string)$xml['name']);
207
208 44
        if ($this->strict) {
209 1
            $ruleSet->setStrict();
210
        }
211
212 44
        foreach ($xml->children() as $node) {
213
            if ($node->getName() === 'php-includepath') {
214 44
                $includePath = (string)$node;
215 1
216
                if (is_dir(dirname($fileName) . DIRECTORY_SEPARATOR . $includePath)) {
217 1
                    $includePath = dirname($fileName) . DIRECTORY_SEPARATOR . $includePath;
218
                    $includePath = realpath($includePath);
219
                }
220
221
                $includePath = get_include_path() . PATH_SEPARATOR . $includePath;
222 1
                set_include_path($includePath);
223 1
            }
224
        }
225
226
        foreach ($xml->children() as $node) {
227 44
            if ($node->getName() === 'description') {
228 44
                $ruleSet->setDescription((string)$node);
229 44
            } elseif ($node->getName() === 'rule') {
230 44
                $this->parseRuleNode($ruleSet, $node);
231 44
            }
232
        }
233
234
        return $ruleSet;
235 42
    }
236
237
    /**
238
     * This method parses a single rule xml node. Bases on the structure of the
239
     * xml node this method delegates the parsing process to another method in
240
     * this class.
241
     *
242
     * @param \PHPMD\RuleSet $ruleSet
243
     * @param \SimpleXMLElement $node
244
     * @return void
245
     */
246
    private function parseRuleNode(RuleSet $ruleSet, \SimpleXMLElement $node)
247 44
    {
248
        if (substr($node['ref'], -3, 3) === 'xml') {
249 44
            $this->parseRuleSetReferenceNode($ruleSet, $node);
250 6
251 6
            return;
252
        }
253 44
        if ('' === (string)$node['ref']) {
254 44
            $this->parseSingleRuleNode($ruleSet, $node);
255 42
256
            return;
257 8
        }
258 8
        $this->parseRuleReferenceNode($ruleSet, $node);
259
    }
260
261
    /**
262
     * This method parses a complete rule set that was includes a reference in
263
     * the currently parsed ruleset.
264
     *
265
     * @param \PHPMD\RuleSet $ruleSet
266
     * @param \SimpleXMLElement $ruleSetNode
267
     * @return void
268 6
     */
269
    private function parseRuleSetReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleSetNode)
270 6
    {
271 6
        $rules = $this->parseRuleSetReference($ruleSetNode);
272 6
        foreach ($rules as $rule) {
273 5
            if ($this->isIncluded($rule, $ruleSetNode)) {
274
                $ruleSet->addRule($rule);
275
            }
276 6
        }
277
    }
278
279
    /**
280
     * Parses a rule-set xml file referenced by the given rule-set xml element.
281
     *
282
     * @param \SimpleXMLElement $ruleSetNode
283
     * @return \PHPMD\RuleSet
284
     * @since 0.2.3
285 6
     */
286
    private function parseRuleSetReference(\SimpleXMLElement $ruleSetNode)
287 6
    {
288 6
        $ruleSetFactory = new RuleSetFactory();
289 6
        $ruleSetFactory->setMinimumPriority($this->minimumPriority);
290
        $ruleSetFactory->setMaximumPriority($this->maximumPriority);
291 6
292
        return $ruleSetFactory->createSingleRuleSet((string)$ruleSetNode['ref']);
293
    }
294
295
    /**
296
     * Checks if the given rule is included/not excluded by the given rule-set
297
     * reference node.
298
     *
299
     * @param \PHPMD\Rule $rule
300
     * @param \SimpleXMLElement $ruleSetNode
301
     * @return boolean
302
     * @since 0.2.3
303 6
     */
304
    private function isIncluded(Rule $rule, \SimpleXMLElement $ruleSetNode)
305 6
    {
306 2
        foreach ($ruleSetNode->exclude as $exclude) {
307 2
            if ($rule->getName() === (string)$exclude['name']) {
308
                return false;
309
            }
310 5
        }
311
312
        return true;
313
    }
314
315
    /**
316
     * This method will create a single rule instance and add it to the given
317
     * {@link \PHPMD\RuleSet} object.
318
     *
319
     * @param \PHPMD\RuleSet $ruleSet
320
     * @param \SimpleXMLElement $ruleNode
321
     * @return void
322
     * @throws RuleClassFileNotFoundException
323 44
     * @throws RuleClassNotFoundException
324
     */
325 44
    private function parseSingleRuleNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
326
    {
327 44
        $fileName = "";
328
329 44
        $ruleSetFolderPath = dirname($ruleSet->getFileName());
330 1
331
        if (isset($ruleNode['file'])) {
332 1
            if (is_readable((string)$ruleNode['file'])) {
333 1
                $fileName = (string)$ruleNode['file'];
334
            } elseif (is_readable($ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'])) {
335
                $fileName = $ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'];
336
            }
337 44
        }
338
339 44
        $className = (string)$ruleNode['class'];
340 44
341
        if (!is_readable($fileName)) {
342
            $fileName = strtr($className, '\\', '/') . '.php';
343 44
        }
344 44
345
        if (!is_readable($fileName)) {
346
            $fileName = str_replace(array('\\', '_'), '/', $className) . '.php';
347 44
        }
348 3
349 3
        if (class_exists($className) === false) {
350 1
            $handle = @fopen($fileName, 'r', true);
351
            if ($handle === false) {
352 2
                throw new RuleClassFileNotFoundException($className);
353
            }
354 2
            fclose($handle);
355
356 2
            include_once $fileName;
357 1
358
            if (class_exists($className) === false) {
359
                throw new RuleClassNotFoundException($className);
360
            }
361
        }
362 42
363 42
        /* @var $rule \PHPMD\Rule */
364 42
        $rule = new $className();
365 42
        $rule->setName((string)$ruleNode['name']);
366
        $rule->setMessage((string)$ruleNode['message']);
367 42
        $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
368
369 42
        $rule->setRuleSetName($ruleSet->getName());
370 42
371
        if (trim($ruleNode['since']) !== '') {
372
            $rule->setSince((string)$ruleNode['since']);
373 42
        }
374
375 42 View Code Duplication
        foreach ($ruleNode->children() as $node) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
376 42
            if ($node->getName() === 'description') {
377 42
                $rule->setDescription((string)$node);
378 41
            } elseif ($node->getName() === 'example') {
379 42
                $rule->addExample((string)$node);
380 42
            } elseif ($node->getName() === 'priority') {
381 42
                $rule->setPriority((integer)$node);
382 42
            } elseif ($node->getName() === 'properties') {
383
                $this->parsePropertiesNode($rule, $node);
384
            }
385
        }
386 42
387 41
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
388
            $ruleSet->addRule($rule);
389 42
        }
390
    }
391
392
    /**
393
     * This method parses a single rule that was included from a different
394
     * rule-set.
395
     *
396
     * @param \PHPMD\RuleSet $ruleSet
397
     * @param \SimpleXMLElement $ruleNode
398
     * @return void
399 8
     */
400
    private function parseRuleReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
401 8
    {
402
        $ref = (string)$ruleNode['ref'];
403 8
404 8
        $fileName = substr($ref, 0, strpos($ref, '.xml/') + 4);
405
        $fileName = $this->createRuleSetFileName($fileName);
406 8
407
        $ruleName = substr($ref, strpos($ref, '.xml/') + 5);
408 8
409
        $ruleSetFactory = new RuleSetFactory();
410 8
411 8
        $ruleSetRef = $ruleSetFactory->createSingleRuleSet($fileName);
412
        $rule = $ruleSetRef->getRuleByName($ruleName);
413 8
414 3
        if (trim($ruleNode['name']) !== '') {
415
            $rule->setName((string)$ruleNode['name']);
416 8
        }
417 3
        if (trim($ruleNode['message']) !== '') {
418
            $rule->setMessage((string)$ruleNode['message']);
419 8
        }
420 3
        if (trim($ruleNode['externalInfoUrl']) !== '') {
421
            $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
422
        }
423 8
424 View Code Duplication
        foreach ($ruleNode->children() as $node) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
425 4
            if ($node->getName() === 'description') {
426 4
                $rule->setDescription((string)$node);
427 4
            } elseif ($node->getName() === 'example') {
428 4
                $rule->addExample((string)$node);
429 4
            } elseif ($node->getName() === 'priority') {
430 4
                $rule->setPriority((integer)$node);
431 4
            } elseif ($node->getName() === 'properties') {
432 4
                $this->parsePropertiesNode($rule, $node);
433
            }
434
        }
435
436 8
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
437 8
            $ruleSet->addRule($rule);
438
        }
439 8
    }
440
441
    /**
442
     * This method parses a xml properties structure and adds all found properties
443
     * to the given <b>$rule</b> object.
444
     *
445
     * <code>
446
     *   ...
447
     *   <properties>
448
     *       <property name="foo" value="42" />
449
     *       <property name="bar" value="23" />
450
     *       ...
451
     *   </properties>
452
     *   ...
453
     * </code>
454
     *
455
     * @param \PHPMD\Rule $rule
456
     * @param \SimpleXMLElement $propertiesNode
457
     * @return void
458
     */
459 42
    private function parsePropertiesNode(Rule $rule, \SimpleXMLElement $propertiesNode)
460
    {
461 42
        foreach ($propertiesNode->children() as $node) {
462
            if ($node->getName() === 'property') {
463 12
                $this->addProperty($rule, $node);
464 12
            }
465
        }
466
    }
467 42
468
    /**
469
     * Adds an additional property to the given <b>$rule</b> instance.
470
     *
471
     * @param \PHPMD\Rule $rule
472
     * @param \SimpleXMLElement $node
473
     * @return void
474
     */
475
    private function addProperty(Rule $rule, \SimpleXMLElement $node)
476 12
    {
477
        $name = trim($node['name']);
478 12
        $value = trim($this->getPropertyValue($node));
479 12
        if ($name !== '' && $value !== '') {
480 12
            $rule->addProperty($name, $value);
481 12
        }
482
    }
483 12
484
    /**
485
     * Returns the value of a property node. This value can be expressed in
486
     * two different notations. First version is an attribute named <b>value</b>
487
     * and the second valid notation is a child element named <b>value</b> that
488
     * contains the value as character data.
489
     *
490
     * @param \SimpleXMLElement $propertyNode
491
     * @return string
492
     * @since 0.2.5
493
     */
494
    private function getPropertyValue(\SimpleXMLElement $propertyNode)
495 12
    {
496
        if (isset($propertyNode->value)) {
497 12
            return (string)$propertyNode->value;
498 1
        }
499
500 11
        return (string)$propertyNode['value'];
501
    }
502
503
    /**
504
     * Returns an array of path exclude patterns in format described at
505
     *
506
     * http://pmd.sourceforge.net/pmd-5.0.4/howtomakearuleset.html#Excluding_files_from_a_ruleset
507
     *
508
     * @param string $fileName The filename of a rule-set definition.
509
     * @return array|null
510
     * @throws RuntimeException Thrown if file is not proper xml
511
     */
512
    public function getIgnorePattern($fileName)
513 8
    {
514
        $excludes = array();
515 8
        foreach (array_map('trim', explode(',', $fileName)) as $ruleSetFileName) {
516 8
            $ruleSetFileName = $this->createRuleSetFileName($ruleSetFileName);
517 8
518
            // Hide error messages
519
            $libxml = libxml_use_internal_errors(true);
520 8
521
            $xml = simplexml_load_string(file_get_contents($ruleSetFileName));
522 8 View Code Duplication
            if ($xml === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
523 8
                // Reset error handling to previous setting
524
                libxml_use_internal_errors($libxml);
525
526
                throw new RuntimeException(trim(libxml_get_last_error()->message));
527
            }
528
529
            foreach ($xml->children() as $node) {
530 8
                if ($node->getName() === 'exclude-pattern') {
531
                    $excludes[] = '' . $node;
532 8
                }
533 2
            }
534
535
            return $excludes;
536
        }
537 8
538
        return null;
539
    }
540
541
    /**
542
     * Checks if given file path exists, is file (or symlink to file)
543
     * and is readable by current user
544
     *
545
     * @param string $filePath File path to check against
546
     * @return bool True if file exists and is readable, false otherwise
547
     */
548
    private function isReadableFile($filePath)
549 48
    {
550
        return is_readable($filePath) && is_file($filePath);
551 48
    }
552 47
553
    /**
554 27
     * Returns list of possible file paths to search against code rules
555
     *
556
     * @param string $fileName Rule set file name
557
     * @return array Array of possible file locations
558
     */
559
    private function filePaths($fileName)
560
    {
561
        $filePathParts = array(
562
            array($fileName),
563 48
            array($this->location, $fileName),
564
            array($this->location, 'rulesets', $fileName . '.xml'),
565
            array(getcwd(), 'rulesets', $fileName . '.xml'),
566 48
        );
567 48
568 48
        foreach (explode(PATH_SEPARATOR, get_include_path()) as $includePath) {
569 48
            $filePathParts[] = array($includePath, $fileName);
570
            $filePathParts[] = array($includePath, $fileName . '.xml');
571
        }
572 48
573 48
        return array_map('implode', array_fill(0, count($filePathParts), DIRECTORY_SEPARATOR), $filePathParts);
574 48
    }
575
}
576