RuleSetFactory::parseRuleSetReferenceNode()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
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 PHPMD\Exception\RuleClassFileNotFoundException;
21
use PHPMD\Exception\RuleClassNotFoundException;
22
use PHPMD\Exception\RuleSetNotFoundException;
23
use RuntimeException;
24
25
/**
26
 * This factory class is used to create the {@link \PHPMD\RuleSet} instance
27
 * that PHPMD will use to analyze the source code.
28
 */
29
class RuleSetFactory
30
{
31
    /**
32
     * Is the strict mode active?
33
     *
34
     * @var boolean
35
     * @since 1.2.0
36
     */
37
    private $strict = false;
38
39
    /**
40
     * The data directory set within the class constructor.
41
     *
42
     * @var string
43
     */
44
    private $location;
45
46
    /**
47
     * The minimum priority for rules to load.
48
     *
49
     * @var integer
50
     */
51
    private $minimumPriority = Rule::LOWEST_PRIORITY;
52
53
    /**
54
     * The maximum priority for rules to load.
55
     *
56
     * @var integer
57
     */
58
    private $maximumPriority = Rule::HIGHEST_PRIORITY;
59
60
    /**
61
     * Constructs a new default rule-set factory instance.
62
     */
63
    public function __construct()
64
    {
65
        $this->location = __DIR__ . '/../../resources';
66
    }
67
68
    /**
69
     * Activates the strict mode for all rule sets.
70
     *
71
     * @return void
72
     * @since 1.2.0
73
     */
74
    public function setStrict()
75
    {
76
        $this->strict = true;
77
    }
78
79
    /**
80
     * Sets the minimum priority that a rule must have.
81
     *
82
     * @param integer $minimumPriority The minimum priority value.
83
     * @return void
84
     */
85
    public function setMinimumPriority($minimumPriority)
86
    {
87
        $this->minimumPriority = $minimumPriority;
88
    }
89
90
    /**
91
     * Sets the maximum priority that a rule must have.
92
     *
93
     * @param integer $maximumPriority The maximum priority value.
94
     * @return void
95
     */
96
    public function setMaximumPriority($maximumPriority)
97
    {
98
        $this->maximumPriority = $maximumPriority;
99
    }
100
101
    /**
102
     * Creates an array of rule-set instances for the given argument.
103
     *
104
     * @param string $ruleSetFileNames Comma-separated string of rule-set filenames or identifier.
105
     * @return \PHPMD\RuleSet[]
106
     */
107
    public function createRuleSets($ruleSetFileNames)
108
    {
109
        $ruleSets = array();
110
111
        $ruleSetFileName = strtok($ruleSetFileNames, ',');
112
        while ($ruleSetFileName !== false) {
113
            $ruleSets[] = $this->createSingleRuleSet($ruleSetFileName);
114
115
            $ruleSetFileName = strtok(',');
116
        }
117
118
        return $ruleSets;
119
    }
120
121
    /**
122
     * Creates a single rule-set instance for the given filename or identifier.
123
     *
124
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
125
     * @return \PHPMD\RuleSet
126
     */
127
    public function createSingleRuleSet($ruleSetOrFileName)
128
    {
129
        $fileName = $this->createRuleSetFileName($ruleSetOrFileName);
130
131
        return $this->parseRuleSetNode($fileName);
132
    }
133
134
    /**
135
     * Lists available rule-set identifiers.
136
     *
137
     * @return string[]
138
     */
139
    public function listAvailableRuleSets()
140
    {
141
        return array_merge(
142
            self::listRuleSetsInDirectory($this->location . '/rulesets/'),
143
            self::listRuleSetsInDirectory(getcwd() . '/rulesets/')
144
        );
145
    }
146
147
    /**
148
     * This method creates the filename for a rule-set identifier or it returns
149
     * the input when it is already a filename.
150
     *
151
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
152
     * @return string Path to rule set file name
153
     * @throws RuleSetNotFoundException Thrown if no readable file found
154
     */
155
    private function createRuleSetFileName($ruleSetOrFileName)
156
    {
157
        foreach ($this->filePaths($ruleSetOrFileName) as $filePath) {
158
            if ($this->isReadableFile($filePath)) {
159
                return $filePath;
160
            }
161
        }
162
163
        throw new RuleSetNotFoundException($ruleSetOrFileName);
164
    }
165
166
    /**
167
     * Lists available rule-set identifiers in given directory.
168
     *
169
     * @param string $directory The directory to scan for rule-sets.
170
     * @return string[]
171
     */
172
    private static function listRuleSetsInDirectory($directory)
173
    {
174
        $ruleSets = array();
175
        if (is_dir($directory)) {
176
            foreach (scandir($directory) as $file) {
177
                $matches = array();
178
                if (is_file($directory . $file) && preg_match('/^(.*)\.xml$/', $file, $matches)) {
179
                    $ruleSets[] = $matches[1];
180
                }
181
            }
182
        }
183
184
        return $ruleSets;
185
    }
186
187
    /**
188
     * This method parses the rule-set definition in the given file.
189
     *
190
     * @param string $fileName
191
     * @return \PHPMD\RuleSet
192
     * @throws RuntimeException When loading the XML file fails.
193
     */
194
    private function parseRuleSetNode($fileName)
195
    {
196
        // Hide error messages
197
        $libxml = libxml_use_internal_errors(true);
198
199
        $xml = simplexml_load_string(file_get_contents($fileName));
200
        if ($xml === false) {
201
            // Reset error handling to previous setting
202
            libxml_use_internal_errors($libxml);
203
204
            throw new RuntimeException(trim(libxml_get_last_error()->message));
205
        }
206
207
        $ruleSet = new RuleSet();
208
        $ruleSet->setFileName($fileName);
209
        $ruleSet->setName((string)$xml['name']);
210
211
        if ($this->strict) {
212
            $ruleSet->setStrict();
213
        }
214
215
        foreach ($xml->children() as $node) {
216
            if ($node->getName() === 'php-includepath') {
217
                $includePath = (string)$node;
218
219
                if (is_dir(dirname($fileName) . DIRECTORY_SEPARATOR . $includePath)) {
220
                    $includePath = dirname($fileName) . DIRECTORY_SEPARATOR . $includePath;
221
                    $includePath = realpath($includePath);
222
                }
223
224
                $includePath = get_include_path() . PATH_SEPARATOR . $includePath;
225
                set_include_path($includePath);
226
            }
227
        }
228
229
        foreach ($xml->children() as $node) {
230
            if ($node->getName() === 'description') {
231
                $ruleSet->setDescription((string)$node);
232
            } elseif ($node->getName() === 'rule') {
233
                $this->parseRuleNode($ruleSet, $node);
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type null; however, parameter $node of PHPMD\RuleSetFactory::parseRuleNode() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
                $this->parseRuleNode($ruleSet, /** @scrutinizer ignore-type */ $node);
Loading history...
234
            }
235
        }
236
237
        return $ruleSet;
238
    }
239
240
    /**
241
     * This method parses a single rule xml node. Bases on the structure of the
242
     * xml node this method delegates the parsing process to another method in
243
     * this class.
244
     *
245
     * @param \PHPMD\RuleSet $ruleSet
246
     * @param \SimpleXMLElement $node
247
     * @return void
248
     */
249
    private function parseRuleNode(RuleSet $ruleSet, \SimpleXMLElement $node)
250
    {
251
        $ref = (string)$node['ref'];
252
253
        if ($ref === '') {
254
            $this->parseSingleRuleNode($ruleSet, $node);
255
256
            return;
257
        }
258
259
        if (substr($ref, -3, 3) === 'xml') {
260
            $this->parseRuleSetReferenceNode($ruleSet, $node);
261
262
            return;
263
        }
264
265
        $this->parseRuleReferenceNode($ruleSet, $node);
266
    }
267
268
    /**
269
     * This method parses a complete rule set that was includes a reference in
270
     * the currently parsed ruleset.
271
     *
272
     * @param \PHPMD\RuleSet $ruleSet
273
     * @param \SimpleXMLElement $ruleSetNode
274
     * @return void
275
     */
276
    private function parseRuleSetReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleSetNode)
277
    {
278
        $rules = $this->parseRuleSetReference($ruleSetNode);
279
        foreach ($rules as $rule) {
280
            if ($this->isIncluded($rule, $ruleSetNode)) {
281
                $ruleSet->addRule($rule);
282
            }
283
        }
284
    }
285
286
    /**
287
     * Parses a rule-set xml file referenced by the given rule-set xml element.
288
     *
289
     * @param \SimpleXMLElement $ruleSetNode
290
     * @return \PHPMD\RuleSet
291
     * @since 0.2.3
292
     */
293
    private function parseRuleSetReference(\SimpleXMLElement $ruleSetNode)
294
    {
295
        $ruleSetFactory = new RuleSetFactory();
296
        $ruleSetFactory->setMinimumPriority($this->minimumPriority);
297
        $ruleSetFactory->setMaximumPriority($this->maximumPriority);
298
299
        return $ruleSetFactory->createSingleRuleSet((string)$ruleSetNode['ref']);
300
    }
301
302
    /**
303
     * Checks if the given rule is included/not excluded by the given rule-set
304
     * reference node.
305
     *
306
     * @param \PHPMD\Rule $rule
307
     * @param \SimpleXMLElement $ruleSetNode
308
     * @return boolean
309
     * @since 0.2.3
310
     */
311
    private function isIncluded(Rule $rule, \SimpleXMLElement $ruleSetNode)
312
    {
313
        foreach ($ruleSetNode->exclude as $exclude) {
314
            if ($rule->getName() === (string)$exclude['name']) {
315
                return false;
316
            }
317
        }
318
319
        return true;
320
    }
321
322
    /**
323
     * This method will create a single rule instance and add it to the given
324
     * {@link \PHPMD\RuleSet} object.
325
     *
326
     * @param \PHPMD\RuleSet $ruleSet
327
     * @param \SimpleXMLElement $ruleNode
328
     * @return void
329
     * @throws RuleClassFileNotFoundException
330
     * @throws RuleClassNotFoundException
331
     */
332
    private function parseSingleRuleNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
333
    {
334
        $fileName = '';
335
336
        $ruleSetFolderPath = dirname($ruleSet->getFileName());
337
338
        if (isset($ruleNode['file'])) {
339
            if (is_readable((string)$ruleNode['file'])) {
340
                $fileName = (string)$ruleNode['file'];
341
            } elseif (is_readable($ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'])) {
342
                $fileName = $ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'];
343
            }
344
        }
345
346
        $className = (string)$ruleNode['class'];
347
348
        if (!is_readable($fileName)) {
349
            $fileName = strtr($className, '\\', '/') . '.php';
350
        }
351
352
        if (!is_readable($fileName)) {
353
            $fileName = str_replace(array('\\', '_'), '/', $className) . '.php';
354
        }
355
356
        if (class_exists($className) === false) {
357
            $handle = @fopen($fileName, 'r', true);
358
            if ($handle === false) {
359
                throw new RuleClassFileNotFoundException($className);
360
            }
361
            fclose($handle);
362
363
            include_once $fileName;
364
365
            if (class_exists($className) === false) {
366
                throw new RuleClassNotFoundException($className);
367
            }
368
        }
369
370
        /* @var $rule \PHPMD\Rule */
371
        $rule = new $className();
372
        $rule->setName((string)$ruleNode['name']);
373
        $rule->setMessage((string)$ruleNode['message']);
374
        $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
375
376
        $rule->setRuleSetName($ruleSet->getName());
377
378
        if (isset($ruleNode['since']) && trim($ruleNode['since']) !== '') {
379
            $rule->setSince((string)$ruleNode['since']);
380
        }
381
382
        foreach ($ruleNode->children() as $node) {
383
            if ($node->getName() === 'description') {
384
                $rule->setDescription((string)$node);
385
            } elseif ($node->getName() === 'example') {
386
                $rule->addExample((string)$node);
387
            } elseif ($node->getName() === 'priority') {
388
                $rule->setPriority((integer)$node);
389
            } elseif ($node->getName() === 'properties') {
390
                $this->parsePropertiesNode($rule, $node);
391
            }
392
        }
393
394
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
395
            $ruleSet->addRule($rule);
396
        }
397
    }
398
399
    /**
400
     * This method parses a single rule that was included from a different
401
     * rule-set.
402
     *
403
     * @param \PHPMD\RuleSet $ruleSet
404
     * @param \SimpleXMLElement $ruleNode
405
     * @return void
406
     */
407
    private function parseRuleReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
408
    {
409
        $ref = (string)$ruleNode['ref'];
410
411
        $fileName = substr($ref, 0, strpos($ref, '.xml/') + 4);
412
        $fileName = $this->createRuleSetFileName($fileName);
413
414
        $ruleName = substr($ref, strpos($ref, '.xml/') + 5);
415
416
        $ruleSetFactory = new RuleSetFactory();
417
418
        $ruleSetRef = $ruleSetFactory->createSingleRuleSet($fileName);
419
        $rule = $ruleSetRef->getRuleByName($ruleName);
420
421
        if (isset($ruleNode['name']) && trim($ruleNode['name']) !== '') {
422
            $rule->setName((string)$ruleNode['name']);
423
        }
424
        if (isset($ruleNode['message']) && trim($ruleNode['message']) !== '') {
425
            $rule->setMessage((string)$ruleNode['message']);
426
        }
427
        if (isset($ruleNode['externalInfoUrl']) && trim($ruleNode['externalInfoUrl']) !== '') {
428
            $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
429
        }
430
431
        foreach ($ruleNode->children() as $node) {
432
            if ($node->getName() === 'description') {
433
                $rule->setDescription((string)$node);
434
            } elseif ($node->getName() === 'example') {
435
                $rule->addExample((string)$node);
436
            } elseif ($node->getName() === 'priority') {
437
                $rule->setPriority((integer)$node);
438
            } elseif ($node->getName() === 'properties') {
439
                $this->parsePropertiesNode($rule, $node);
440
            }
441
        }
442
443
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
444
            $ruleSet->addRule($rule);
445
        }
446
    }
447
448
    /**
449
     * This method parses a xml properties structure and adds all found properties
450
     * to the given <b>$rule</b> object.
451
     *
452
     * <code>
453
     *   ...
454
     *   <properties>
455
     *       <property name="foo" value="42" />
456
     *       <property name="bar" value="23" />
457
     *       ...
458
     *   </properties>
459
     *   ...
460
     * </code>
461
     *
462
     * @param \PHPMD\Rule $rule
463
     * @param \SimpleXMLElement $propertiesNode
464
     * @return void
465
     */
466
    private function parsePropertiesNode(Rule $rule, \SimpleXMLElement $propertiesNode)
467
    {
468
        foreach ($propertiesNode->children() as $node) {
469
            if ($node->getName() === 'property') {
470
                $this->addProperty($rule, $node);
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type null; however, parameter $node of PHPMD\RuleSetFactory::addProperty() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

470
                $this->addProperty($rule, /** @scrutinizer ignore-type */ $node);
Loading history...
471
            }
472
        }
473
    }
474
475
    /**
476
     * Adds an additional property to the given <b>$rule</b> instance.
477
     *
478
     * @param \PHPMD\Rule $rule
479
     * @param \SimpleXMLElement $node
480
     * @return void
481
     */
482
    private function addProperty(Rule $rule, \SimpleXMLElement $node)
483
    {
484
        $name = trim($node['name']);
485
        $value = trim($this->getPropertyValue($node));
486
        if ($name !== '' && $value !== '') {
487
            $rule->addProperty($name, $value);
488
        }
489
    }
490
491
    /**
492
     * Returns the value of a property node. This value can be expressed in
493
     * two different notations. First version is an attribute named <b>value</b>
494
     * and the second valid notation is a child element named <b>value</b> that
495
     * contains the value as character data.
496
     *
497
     * @param \SimpleXMLElement $propertyNode
498
     * @return string
499
     * @since 0.2.5
500
     */
501
    private function getPropertyValue(\SimpleXMLElement $propertyNode)
502
    {
503
        if (isset($propertyNode->value)) {
504
            return (string)$propertyNode->value;
505
        }
506
507
        return (string)$propertyNode['value'];
508
    }
509
510
    /**
511
     * Returns an array of path exclude patterns in format described at
512
     *
513
     * http://pmd.sourceforge.net/pmd-5.0.4/howtomakearuleset.html#Excluding_files_from_a_ruleset
514
     *
515
     * @param string $fileName The filename of a rule-set definition.
516
     * @return array|null
517
     * @throws RuntimeException Thrown if file is not proper xml
518
     */
519
    public function getIgnorePattern($fileName)
520
    {
521
        $excludes = array();
522
        foreach (array_map('trim', explode(',', $fileName)) as $ruleSetFileName) {
523
            $ruleSetFileName = $this->createRuleSetFileName($ruleSetFileName);
524
525
            // Hide error messages
526
            $libxml = libxml_use_internal_errors(true);
527
528
            $xml = simplexml_load_string(file_get_contents($ruleSetFileName));
529
            if ($xml === false) {
530
                // Reset error handling to previous setting
531
                libxml_use_internal_errors($libxml);
532
533
                throw new RuntimeException(trim(libxml_get_last_error()->message));
534
            }
535
536
            foreach ($xml->children() as $node) {
537
                if ($node->getName() === 'exclude-pattern') {
538
                    $excludes[] = '' . $node;
539
                }
540
            }
541
542
            return $excludes;
543
        }
544
545
        return null;
546
    }
547
548
    /**
549
     * Checks if given file path exists, is file (or symlink to file)
550
     * and is readable by current user
551
     *
552
     * @param string $filePath File path to check against
553
     * @return bool True if file exists and is readable, false otherwise
554
     */
555
    private function isReadableFile($filePath)
556
    {
557
        return is_readable($filePath) && is_file($filePath);
558
    }
559
560
    /**
561
     * Returns list of possible file paths to search against code rules
562
     *
563
     * @param string $fileName Rule set file name
564
     * @return array Array of possible file locations
565
     */
566
    private function filePaths($fileName)
567
    {
568
        $filePathParts = array(
569
            array($fileName),
570
            array($this->location, $fileName),
571
            array($this->location, 'rulesets', $fileName . '.xml'),
572
            array(getcwd(), 'rulesets', $fileName . '.xml'),
573
        );
574
575
        foreach (explode(PATH_SEPARATOR, get_include_path()) as $includePath) {
576
            $filePathParts[] = array($includePath, $fileName);
577
            $filePathParts[] = array($includePath, $fileName . '.xml');
578
        }
579
580
        return array_map('implode', array_fill(0, count($filePathParts), DIRECTORY_SEPARATOR), $filePathParts);
581
    }
582
}
583