Passed
Push — master ( 902a34...c826a7 )
by Kyle
53s queued 11s
created

src/main/php/PHPMD/RuleSetFactory.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
/**
21
 * This factory class is used to create the {@link \PHPMD\RuleSet} instance
22
 * that PHPMD will use to analyze the source code.
23
 */
24
class RuleSetFactory
25
{
26
    /**
27
     * Is the strict mode active?
28
     *
29
     * @var boolean
30
     * @since 1.2.0
31
     */
32
    private $strict = false;
33
34
    /**
35
     * The data directory set within the class constructor.
36
     *
37
     * @var string
38
     */
39
    private $location;
40
41
    /**
42
     * The minimum priority for rules to load.
43
     *
44
     * @var integer
45
     */
46
    private $minimumPriority = Rule::LOWEST_PRIORITY;
47
48
    /**
49
     * The maximum priority for rules to load.
50
     *
51
     * @var integer
52
     */
53
    private $maximumPriority = Rule::HIGHEST_PRIORITY;
54
55
    /**
56
     * Constructs a new default rule-set factory instance.
57
     */
58
    public function __construct()
59 48
    {
60
        $this->location = __DIR__ . '/../../resources';
61
    }
62 48
63 48
    /**
64 48
     * Activates the strict mode for all rule sets.
65
     *
66
     * @return void
67
     * @since 1.2.0
68
     */
69
    public function setStrict()
70
    {
71
        $this->strict = true;
72
    }
73
74
    /**
75 1
     * Sets the minimum priority that a rule must have.
76
     *
77 1
     * @param integer $minimumPriority The minimum priority value.
78 1
     * @return void
79
     */
80
    public function setMinimumPriority($minimumPriority)
81
    {
82
        $this->minimumPriority = $minimumPriority;
83
    }
84
85
    /**
86 12
     * Sets the maximum priority that a rule must have.
87
     *
88 12
     * @param integer $maximumPriority The maximum priority value.
89 12
     * @return void
90
     */
91
    public function setMaximumPriority($maximumPriority)
92
    {
93
        $this->maximumPriority = $maximumPriority;
94
    }
95
96
    /**
97 12
     * Creates an array of rule-set instances for the given argument.
98
     *
99 12
     * @param string $ruleSetFileNames Comma-separated string of rule-set filenames or identifier.
100 12
     * @return \PHPMD\RuleSet[]
101
     */
102
    public function createRuleSets($ruleSetFileNames)
103
    {
104
        $ruleSets = array();
105
106
        $ruleSetFileName = strtok($ruleSetFileNames, ',');
107
        while ($ruleSetFileName !== false) {
108 40
            $ruleSets[] = $this->createSingleRuleSet($ruleSetFileName);
109
110 40
            $ruleSetFileName = strtok(',');
111
        }
112 40
113 40
        return $ruleSets;
114 40
    }
115
116 36
    /**
117
     * Creates a single rule-set instance for the given filename or identifier.
118 36
     *
119
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
120
     * @return \PHPMD\RuleSet
121
     */
122
    public function createSingleRuleSet($ruleSetOrFileName)
123
    {
124
        $fileName = $this->createRuleSetFileName($ruleSetOrFileName);
125
126
        return $this->parseRuleSetNode($fileName);
127 46
    }
128
129 46
    /**
130 45
     * Lists available rule-set identifiers.
131
     *
132
     * @return string[]
133
     */
134
    public function listAvailableRuleSets()
135
    {
136
        return array_merge(
137
            self::listRuleSetsInDirectory($this->location . '/rulesets/'),
138 4
            self::listRuleSetsInDirectory(getcwd() . '/rulesets/')
139
        );
140 4
    }
141 4
142 4
    /**
143
     * This method creates the filename for a rule-set identifier or it returns
144
     * the input when it is already a filename.
145
     *
146
     * @param string $ruleSetOrFileName The rule-set filename or identifier.
147
     * @return string Path to rule set file name
148
     * @throws RuleSetNotFoundException Thrown if no readable file found
149
     */
150
    private function createRuleSetFileName($ruleSetOrFileName)
151
    {
152
        foreach ($this->filePaths($ruleSetOrFileName) as $filePath) {
153
            if ($this->isReadableFile($filePath)) {
154 48
                return $filePath;
155
            }
156 48
        }
157 48
158 47
        throw new RuleSetNotFoundException($ruleSetOrFileName);
159
    }
160
161
    /**
162 2
     * Lists available rule-set identifiers in given directory.
163
     *
164
     * @param string $directory The directory to scan for rule-sets.
165
     * @return string[]
166
     */
167
    private static function listRuleSetsInDirectory($directory)
168
    {
169
        $ruleSets = array();
170
        if (is_dir($directory)) {
171 4
            foreach (scandir($directory) as $file) {
172
                $matches = array();
173 4
                if (is_file($directory . $file) && preg_match('/^(.*)\.xml$/', $file, $matches)) {
174 4
                    $ruleSets[] = $matches[1];
175 4
                }
176 4
            }
177 4
        }
178 4
179
        return $ruleSets;
180
    }
181
182 4
    /**
183
     * This method parses the rule-set definition in the given file.
184
     *
185
     * @param string $fileName
186
     * @return \PHPMD\RuleSet
187
     * @throws \RuntimeException When loading the XML file fails.
188
     */
189
    private function parseRuleSetNode($fileName)
190
    {
191 45
        // Hide error messages
192
        $libxml = libxml_use_internal_errors(true);
193
194 45
        $xml = simplexml_load_string(file_get_contents($fileName));
195 View Code Duplication
        if ($xml === false) {
0 ignored issues
show
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...
196 45
            // Reset error handling to previous setting
197 45
            libxml_use_internal_errors($libxml);
198
199 1
            throw new \RuntimeException(trim(libxml_get_last_error()->message));
200
        }
201 1
202
        $ruleSet = new RuleSet();
203
        $ruleSet->setFileName($fileName);
204 44
        $ruleSet->setName((string)$xml['name']);
205 44
206 44
        if ($this->strict) {
207
            $ruleSet->setStrict();
208 44
        }
209 1
210
        foreach ($xml->children() as $node) {
211
            if ($node->getName() === 'php-includepath') {
212 44
                $includePath = (string)$node;
213
214 44
                if (is_dir(dirname($fileName) . DIRECTORY_SEPARATOR . $includePath)) {
215 1
                    $includePath = dirname($fileName) . DIRECTORY_SEPARATOR . $includePath;
216
                    $includePath = realpath($includePath);
217 1
                }
218
219
                $includePath = get_include_path() . PATH_SEPARATOR . $includePath;
220
                set_include_path($includePath);
221
            }
222 1
        }
223 1
224
        foreach ($xml->children() as $node) {
225
            if ($node->getName() === 'description') {
226
                $ruleSet->setDescription((string)$node);
227 44
            } elseif ($node->getName() === 'rule') {
228 44
                $this->parseRuleNode($ruleSet, $node);
229 44
            }
230 44
        }
231 44
232
        return $ruleSet;
233
    }
234
235 42
    /**
236
     * This method parses a single rule xml node. Bases on the structure of the
237
     * xml node this method delegates the parsing process to another method in
238
     * this class.
239
     *
240
     * @param \PHPMD\RuleSet $ruleSet
241
     * @param \SimpleXMLElement $node
242
     * @return void
243
     */
244
    private function parseRuleNode(RuleSet $ruleSet, \SimpleXMLElement $node)
245
    {
246
        if (substr($node['ref'], -3, 3) === 'xml') {
247 44
            $this->parseRuleSetReferenceNode($ruleSet, $node);
248
249 44
            return;
250 6
        }
251 6
        if ('' === (string)$node['ref']) {
252
            $this->parseSingleRuleNode($ruleSet, $node);
253 44
254 44
            return;
255 42
        }
256
        $this->parseRuleReferenceNode($ruleSet, $node);
257 8
    }
258 8
259
    /**
260
     * This method parses a complete rule set that was includes a reference in
261
     * the currently parsed ruleset.
262
     *
263
     * @param \PHPMD\RuleSet $ruleSet
264
     * @param \SimpleXMLElement $ruleSetNode
265
     * @return void
266
     */
267
    private function parseRuleSetReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleSetNode)
268 6
    {
269
        $rules = $this->parseRuleSetReference($ruleSetNode);
270 6
        foreach ($rules as $rule) {
271 6
            if ($this->isIncluded($rule, $ruleSetNode)) {
272 6
                $ruleSet->addRule($rule);
273 5
            }
274
        }
275
    }
276 6
277
    /**
278
     * Parses a rule-set xml file referenced by the given rule-set xml element.
279
     *
280
     * @param \SimpleXMLElement $ruleSetNode
281
     * @return \PHPMD\RuleSet
282
     * @since 0.2.3
283
     */
284
    private function parseRuleSetReference(\SimpleXMLElement $ruleSetNode)
285 6
    {
286
        $ruleSetFactory = new RuleSetFactory();
287 6
        $ruleSetFactory->setMinimumPriority($this->minimumPriority);
288 6
        $ruleSetFactory->setMaximumPriority($this->maximumPriority);
289 6
290
        return $ruleSetFactory->createSingleRuleSet((string)$ruleSetNode['ref']);
291 6
    }
292
293
    /**
294
     * Checks if the given rule is included/not excluded by the given rule-set
295
     * reference node.
296
     *
297
     * @param \PHPMD\Rule $rule
298
     * @param \SimpleXMLElement $ruleSetNode
299
     * @return boolean
300
     * @since 0.2.3
301
     */
302
    private function isIncluded(Rule $rule, \SimpleXMLElement $ruleSetNode)
303 6
    {
304
        foreach ($ruleSetNode->exclude as $exclude) {
305 6
            if ($rule->getName() === (string)$exclude['name']) {
306 2
                return false;
307 2
            }
308
        }
309
310 5
        return true;
311
    }
312
313
    /**
314
     * This method will create a single rule instance and add it to the given
315
     * {@link \PHPMD\RuleSet} object.
316
     *
317
     * @param \PHPMD\RuleSet $ruleSet
318
     * @param \SimpleXMLElement $ruleNode
319
     * @return void
320
     * @throws RuleClassFileNotFoundException
321
     * @throws RuleClassNotFoundException
322
     */
323 44
    private function parseSingleRuleNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
324
    {
325 44
        $fileName = "";
326
327 44
        $ruleSetFolderPath = dirname($ruleSet->getFileName());
328
329 44
        if (isset($ruleNode['file'])) {
330 1
            if (is_readable((string)$ruleNode['file'])) {
331
                $fileName = (string)$ruleNode['file'];
332 1
            } elseif (is_readable($ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'])) {
333 1
                $fileName = $ruleSetFolderPath . DIRECTORY_SEPARATOR . (string)$ruleNode['file'];
334
            }
335
        }
336
337 44
        $className = (string)$ruleNode['class'];
338
339 44
        if (!is_readable($fileName)) {
340 44
            $fileName = strtr($className, '\\', '/') . '.php';
341
        }
342
343 44
        if (!is_readable($fileName)) {
344 44
            $fileName = str_replace(array('\\', '_'), '/', $className) . '.php';
345
        }
346
347 44
        if (class_exists($className) === false) {
348 3
            $handle = @fopen($fileName, 'r', true);
349 3
            if ($handle === false) {
350 1
                throw new RuleClassFileNotFoundException($className);
351
            }
352 2
            fclose($handle);
353
354 2
            include_once $fileName;
355
356 2
            if (class_exists($className) === false) {
357 1
                throw new RuleClassNotFoundException($className);
358
            }
359
        }
360
361
        /* @var $rule \PHPMD\Rule */
362 42
        $rule = new $className();
363 42
        $rule->setName((string)$ruleNode['name']);
364 42
        $rule->setMessage((string)$ruleNode['message']);
365 42
        $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
366
367 42
        $rule->setRuleSetName($ruleSet->getName());
368
369 42
        if (trim($ruleNode['since']) !== '') {
370 42
            $rule->setSince((string)$ruleNode['since']);
371
        }
372
373 42 View Code Duplication
        foreach ($ruleNode->children() as $node) {
0 ignored issues
show
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...
374
            if ($node->getName() === 'description') {
375 42
                $rule->setDescription((string)$node);
376 42
            } elseif ($node->getName() === 'example') {
377 42
                $rule->addExample((string)$node);
378 41
            } elseif ($node->getName() === 'priority') {
379 42
                $rule->setPriority((integer)$node);
380 42
            } elseif ($node->getName() === 'properties') {
381 42
                $this->parsePropertiesNode($rule, $node);
382 42
            }
383
        }
384
385
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
386 42
            $ruleSet->addRule($rule);
387 41
        }
388
    }
389 42
390
    /**
391
     * This method parses a single rule that was included from a different
392
     * rule-set.
393
     *
394
     * @param \PHPMD\RuleSet $ruleSet
395
     * @param \SimpleXMLElement $ruleNode
396
     * @return void
397
     */
398
    private function parseRuleReferenceNode(RuleSet $ruleSet, \SimpleXMLElement $ruleNode)
399 8
    {
400
        $ref = (string)$ruleNode['ref'];
401 8
402
        $fileName = substr($ref, 0, strpos($ref, '.xml/') + 4);
403 8
        $fileName = $this->createRuleSetFileName($fileName);
404 8
405
        $ruleName = substr($ref, strpos($ref, '.xml/') + 5);
406 8
407
        $ruleSetFactory = new RuleSetFactory();
408 8
409
        $ruleSetRef = $ruleSetFactory->createSingleRuleSet($fileName);
410 8
        $rule = $ruleSetRef->getRuleByName($ruleName);
411 8
412
        if (trim($ruleNode['name']) !== '') {
413 8
            $rule->setName((string)$ruleNode['name']);
414 3
        }
415
        if (trim($ruleNode['message']) !== '') {
416 8
            $rule->setMessage((string)$ruleNode['message']);
417 3
        }
418
        if (trim($ruleNode['externalInfoUrl']) !== '') {
419 8
            $rule->setExternalInfoUrl((string)$ruleNode['externalInfoUrl']);
420 3
        }
421
422 View Code Duplication
        foreach ($ruleNode->children() as $node) {
0 ignored issues
show
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...
423 8
            if ($node->getName() === 'description') {
424
                $rule->setDescription((string)$node);
425 4
            } elseif ($node->getName() === 'example') {
426 4
                $rule->addExample((string)$node);
427 4
            } elseif ($node->getName() === 'priority') {
428 4
                $rule->setPriority((integer)$node);
429 4
            } elseif ($node->getName() === 'properties') {
430 4
                $this->parsePropertiesNode($rule, $node);
431 4
            }
432 4
        }
433
434
        if ($rule->getPriority() <= $this->minimumPriority && $rule->getPriority() >= $this->maximumPriority) {
435
            $ruleSet->addRule($rule);
436 8
        }
437 8
    }
438
439 8
    /**
440
     * This method parses a xml properties structure and adds all found properties
441
     * to the given <b>$rule</b> object.
442
     *
443
     * <code>
444
     *   ...
445
     *   <properties>
446
     *       <property name="foo" value="42" />
447
     *       <property name="bar" value="23" />
448
     *       ...
449
     *   </properties>
450
     *   ...
451
     * </code>
452
     *
453
     * @param \PHPMD\Rule $rule
454
     * @param \SimpleXMLElement $propertiesNode
455
     * @return void
456
     */
457
    private function parsePropertiesNode(Rule $rule, \SimpleXMLElement $propertiesNode)
458
    {
459 42
        foreach ($propertiesNode->children() as $node) {
460
            if ($node->getName() === 'property') {
461 42
                $this->addProperty($rule, $node);
462
            }
463 12
        }
464 12
    }
465
466
    /**
467 42
     * Adds an additional property to the given <b>$rule</b> instance.
468
     *
469
     * @param \PHPMD\Rule $rule
470
     * @param \SimpleXMLElement $node
471
     * @return void
472
     */
473
    private function addProperty(Rule $rule, \SimpleXMLElement $node)
474
    {
475
        $name = trim($node['name']);
476 12
        $value = trim($this->getPropertyValue($node));
477
        if ($name !== '' && $value !== '') {
478 12
            $rule->addProperty($name, $value);
479 12
        }
480 12
    }
481 12
482
    /**
483 12
     * Returns the value of a property node. This value can be expressed in
484
     * two different notations. First version is an attribute named <b>value</b>
485
     * and the second valid notation is a child element named <b>value</b> that
486
     * contains the value as character data.
487
     *
488
     * @param \SimpleXMLElement $propertyNode
489
     * @return string
490
     * @since 0.2.5
491
     */
492
    private function getPropertyValue(\SimpleXMLElement $propertyNode)
493
    {
494
        if (isset($propertyNode->value)) {
495 12
            return (string)$propertyNode->value;
496
        }
497 12
498 1
        return (string)$propertyNode['value'];
499
    }
500 11
501
    /**
502
     * Returns an array of path exclude patterns in format described at
503
     *
504
     * http://pmd.sourceforge.net/pmd-5.0.4/howtomakearuleset.html#Excluding_files_from_a_ruleset
505
     *
506
     * @param string $fileName The filename of a rule-set definition.
507
     * @return array|null
508
     * @throws \RuntimeException Thrown if file is not proper xml
509
     */
510
    public function getIgnorePattern($fileName)
511
    {
512
        $excludes = array();
513 8
        foreach (array_map('trim', explode(',', $fileName)) as $ruleSetFileName) {
514
            $ruleSetFileName = $this->createRuleSetFileName($ruleSetFileName);
515 8
516 8
            // Hide error messages
517 8
            $libxml = libxml_use_internal_errors(true);
518
519
            $xml = simplexml_load_string(file_get_contents($ruleSetFileName));
520 8 View Code Duplication
            if ($xml === false) {
0 ignored issues
show
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...
521
                // Reset error handling to previous setting
522 8
                libxml_use_internal_errors($libxml);
523 8
524
                throw new \RuntimeException(trim(libxml_get_last_error()->message));
525
            }
526
527
            foreach ($xml->children() as $node) {
528
                if ($node->getName() === 'exclude-pattern') {
529
                    $excludes[] = '' . $node;
530 8
                }
531
            }
532 8
533 2
            return $excludes;
534
        }
535
536
        return null;
537 8
    }
538
539
    /**
540
     * Checks if given file path exists, is file (or symlink to file)
541
     * and is readable by current user
542
     *
543
     * @param string $filePath File path to check against
544
     * @return bool True if file exists and is readable, false otherwise
545
     */
546
    private function isReadableFile($filePath)
547
    {
548
        return is_readable($filePath) && is_file($filePath);
549 48
    }
550
551 48
    /**
552 47
     * Returns list of possible file paths to search against code rules
553
     *
554 27
     * @param string $fileName Rule set file name
555
     * @return array Array of possible file locations
556
     */
557
    private function filePaths($fileName)
558
    {
559
        $filePathParts = array(
560
            array($fileName),
561
            array($this->location, $fileName),
562
            array($this->location, 'rulesets', $fileName . '.xml'),
563 48
            array(getcwd(), 'rulesets', $fileName . '.xml'),
564
        );
565
566 48
        foreach (explode(PATH_SEPARATOR, get_include_path()) as $includePath) {
567 48
            $filePathParts[] = array($includePath, $fileName);
568 48
            $filePathParts[] = array($includePath, $fileName . '.xml');
569 48
        }
570
571
        return array_map('implode', array_fill(0, count($filePathParts), DIRECTORY_SEPARATOR), $filePathParts);
572 48
    }
573
}
574