Passed
Push — main ( 4889b2...808ce0 )
by Michiel
07:43
created

Project::tsort()   B

Complexity

Conditions 11
Paths 32

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11.2865

Importance

Changes 0
Metric Value
eloc 31
dl 0
loc 50
ccs 26
cts 30
cp 0.8667
rs 7.3166
c 0
b 0
f 0
cc 11
nc 32
nop 4
crap 11.2865

How to fix   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 SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
namespace Phing;
21
22
use Exception;
23
use Phing\Exception\BuildException;
24
use Phing\Input\InputHandler;
25
use Phing\Io\File;
26
use Phing\Io\FileSystem;
27
use Phing\Io\FileUtils;
28
use Phing\Io\IOException;
29
use Phing\Listener\BuildEvent;
30
use Phing\Listener\BuildListener;
31
use Phing\Parser\ProjectConfigurator;
32
use Phing\Task\System\Condition\Condition;
33
use Phing\Type\Description;
34
use Phing\Type\PropertyValue;
35
use ReflectionException;
36
use ReflectionObject;
37
38
/**
39
 *  The Phing project class. Represents a completely configured Phing project.
40
 *  The class defines the project and all tasks/targets. It also contains
41
 *  methods to start a build as well as some properties and FileSystem
42
 *  abstraction.
43
 *
44
 * @author Andreas Aderhold <[email protected]>
45
 * @author Hans Lellelid <[email protected]>
46
 *
47
 */
48
class Project
49
{
50
    // Logging level constants.
51
    public const MSG_DEBUG = 4;
52
    public const MSG_VERBOSE = 3;
53
    public const MSG_INFO = 2;
54
    public const MSG_WARN = 1;
55
    public const MSG_ERR = 0;
56
57
    /**
58
     * contains the targets
59
     *
60
     * @var Target[]
61
     */
62
    private $targets = [];
63
    /**
64
     * global filterset (future use)
65
     */
66
    private $globalFilterSet = [];
67
    /**
68
     * all globals filters (future use)
69
     */
70
    private $globalFilters = [];
0 ignored issues
show
introduced by
The private property $globalFilters is not used, and could be removed.
Loading history...
71
72
    /**
73
     * holds ref names and a reference to the referred object
74
     */
75
    private $references = [];
76
77
    /**
78
     * The InputHandler being used by this project.
79
     *
80
     * @var InputHandler
81
     */
82
    private $inputHandler;
83
84
    /* -- properties that come in via xml attributes -- */
85
86
    /**
87
     * basedir (PhingFile object)
88
     */
89
    private $basedir;
90
91
    /**
92
     * the default target name
93
     */
94
    private $defaultTarget = 'all';
95
96
    /**
97
     * project name (required)
98
     */
99
    private $name;
100
101
    /**
102
     * project description
103
     */
104
    private $description;
105
106
    /**
107
     * require phing version
108
     */
109
    private $phingVersion;
110
111
    /**
112
     * project strict mode
113
     */
114
    private $strictMode = false;
115
116
    /**
117
     * a FileUtils object
118
     */
119
    private $fileUtils;
120
121
    /**
122
     * Build listeneers
123
     */
124
    private $listeners = [];
125
126
    /**
127
     * Keep going flag.
128
     */
129
    private $keepGoingMode = false;
130
131
    /**
132
     * @var string[]
133
     */
134
    private $executedTargetNames = [];
135
136
    /**
137
     *  Constructor, sets any default vars.
138
     */
139 887
    public function __construct()
140
    {
141 887
        $this->fileUtils = new FileUtils();
142 887
    }
143
144
    /**
145
     * Sets the input handler
146
     *
147
     * @param InputHandler $handler
148
     */
149 33
    public function setInputHandler($handler)
150
    {
151 33
        $this->inputHandler = $handler;
152 33
    }
153
154
    /**
155
     * Retrieves the current input handler.
156
     *
157
     * @return InputHandler
158
     */
159 33
    public function getInputHandler()
160
    {
161 33
        return $this->inputHandler;
162
    }
163
164
    /**
165
     * inits the project, called from main app
166
     */
167 846
    public function init()
168
    {
169
        // set builtin properties
170 846
        $this->setSystemProperties();
171
172 846
        $componentHelper = ComponentHelper::getComponentHelper($this);
173
174 846
        $componentHelper->initDefaultDefinitions();
175 846
    }
176
177
    /**
178
     * Create and initialize a subproject. By default the subproject will be of
179
     * the same type as its parent. If a no-arg constructor is unavailable, the
180
     * <code>Project</code> class will be used.
181
     * @return Project instance configured as a subproject of this Project.
182
     */
183 36
    public function createSubProject(): Project
184
    {
185
        try {
186 36
            $ref = new ReflectionObject($this);
187 36
            $subProject = $ref->newInstance();
188
        } catch (ReflectionException $e) {
189
            $subProject = new Project();
190
        }
191 36
        $this->initSubProject($subProject);
192 36
        return $subProject;
193
    }
194
195
    /**
196
     * Initialize a subproject.
197
     * @param Project $subProject the subproject to initialize.
198
     */
199 36
    public function initSubProject(Project $subProject): void
200
    {
201 36
        ComponentHelper::getComponentHelper($subProject)
202 36
            ->initSubProject(ComponentHelper::getComponentHelper($this));
203 36
        $subProject->setKeepGoingMode($this->isKeepGoingMode());
204 36
        $subProject->setStrictMode($this->strictMode);
205 36
    }
206
207
    /**
208
     * returns the global filterset (future use)
209
     */
210
    public function getGlobalFilterSet()
211
    {
212
        return $this->globalFilterSet;
213
    }
214
215
    // ---------------------------------------------------------
216
    // Property methods
217
    // ---------------------------------------------------------
218
219
    /**
220
     * Sets a property. Any existing property of the same name
221
     * is overwritten, unless it is a user property.
222
     *
223
     * @param string $name The name of property to set.
224
     *                       Must not be <code>null</code>.
225
     * @param string $value The new value of the property.
226
     *                       Must not be <code>null</code>.
227
     */
228 251
    public function setProperty($name, $value)
229
    {
230 251
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, true);
231 251
    }
232
233
    /**
234
     * Sets a property if no value currently exists. If the property
235
     * exists already, a message is logged and the method returns with
236
     * no other effect.
237
     *
238
     * @param string $name The name of property to set.
239
     *                      Must not be <code>null</code>.
240
     * @param string $value The new value of the property.
241
     *                      Must not be <code>null</code>.
242
     * @since 2.0
243
     */
244 93
    public function setNewProperty($name, $value)
245
    {
246 93
        PropertyHelper::getPropertyHelper($this)->setNewProperty(null, $name, $value);
247 93
    }
248
249
    /**
250
     * Sets a user property, which cannot be overwritten by
251
     * set/unset property calls. Any previous value is overwritten.
252
     *
253
     * @param string $name The name of property to set.
254
     *                      Must not be <code>null</code>.
255
     * @param string $value The new value of the property.
256
     *                      Must not be <code>null</code>.
257
     * @see   setProperty()
258
     */
259 846
    public function setUserProperty($name, $value)
260
    {
261 846
        PropertyHelper::getPropertyHelper($this)->setUserProperty(null, $name, $value);
262 846
    }
263
264
    /**
265
     * Sets a user property, which cannot be overwritten by set/unset
266
     * property calls. Any previous value is overwritten. Also marks
267
     * these properties as properties that have not come from the
268
     * command line.
269
     *
270
     * @param string $name The name of property to set.
271
     *                      Must not be <code>null</code>.
272
     * @param string $value The new value of the property.
273
     *                      Must not be <code>null</code>.
274
     * @see   setProperty()
275
     */
276 12
    public function setInheritedProperty($name, $value)
277
    {
278 12
        PropertyHelper::getPropertyHelper($this)->setInheritedProperty(null, $name, $value);
279 12
    }
280
281
    /**
282
     * Sets a property unless it is already defined as a user property
283
     * (in which case the method returns silently).
284
     *
285
     * @param string $name The name of the property.
286
     *                      Must not be
287
     *                      <code>null</code>.
288
     * @param string $value The property value. Must not be <code>null</code>.
289
     */
290 862
    private function setPropertyInternal($name, $value)
291
    {
292 862
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, false);
293 862
    }
294
295
    /**
296
     * Returns the value of a property, if it is set.
297
     *
298
     * @param string $name The name of the property.
299
     *                      May be <code>null</code>, in which case
300
     *                      the return value is also <code>null</code>.
301
     * @return string The property value, or <code>null</code> for no match
302
     *                     or if a <code>null</code> name is provided.
303
     */
304 846
    public function getProperty($name)
305
    {
306 846
        return PropertyHelper::getPropertyHelper($this)->getProperty(null, $name);
307
    }
308
309
    /**
310
     * Replaces ${} style constructions in the given value with the
311
     * string value of the corresponding data types.
312
     *
313
     * @param string $value The value string to be scanned for property references.
314
     *                      May be <code>null</code>.
315
     *
316
     * @return string the given string with embedded property names replaced
317
     *                by values, or <code>null</code> if the given string is
318
     *                <code>null</code>.
319
     *
320
     * @throws BuildException if the given value has an unclosed
321
     *                           property name, e.g. <code>${xxx</code>
322
     */
323 702
    public function replaceProperties($value)
324
    {
325 702
        return PropertyHelper::getPropertyHelper($this)->replaceProperties($value, $this->getProperties());
326
    }
327
328
    /**
329
     * Returns the value of a user property, if it is set.
330
     *
331
     * @param string $name The name of the property.
332
     *                      May be <code>null</code>, in which case
333
     *                      the return value is also <code>null</code>.
334
     * @return string The property value, or <code>null</code> for no match
335
     *                     or if a <code>null</code> name is provided.
336
     */
337 1
    public function getUserProperty($name)
338
    {
339 1
        return PropertyHelper::getPropertyHelper($this)->getUserProperty(null, $name);
340
    }
341
342
    /**
343
     * Returns a copy of the properties table.
344
     *
345
     * @return array A hashtable containing all properties
346
     *               (including user properties).
347
     */
348 847
    public function getProperties()
349
    {
350 847
        return PropertyHelper::getPropertyHelper($this)->getProperties();
351
    }
352
353
    /**
354
     * Returns a copy of the user property hashtable
355
     *
356
     * @return array a hashtable containing just the user properties
357
     */
358
    public function getUserProperties()
359
    {
360
        return PropertyHelper::getPropertyHelper($this)->getUserProperties();
361
    }
362
363
    public function getInheritedProperties()
364
    {
365
        return PropertyHelper::getPropertyHelper($this)->getInheritedProperties();
366
    }
367
368
    /**
369
     * Copies all user properties that have been set on the command
370
     * line or a GUI tool from this instance to the Project instance
371
     * given as the argument.
372
     *
373
     * <p>To copy all "user" properties, you will also have to call
374
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
375
     *
376
     * @param Project $other the project to copy the properties to.  Must not be null.
377
     * @return void
378
     * @since  phing 2.0
379
     */
380 33
    public function copyUserProperties(Project $other)
381
    {
382 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
383 33
    }
384
385
    /**
386
     * Copies all user properties that have not been set on the
387
     * command line or a GUI tool from this instance to the Project
388
     * instance given as the argument.
389
     *
390
     * <p>To copy all "user" properties, you will also have to call
391
     * {@link #copyUserProperties copyUserProperties}.</p>
392
     *
393
     * @param Project $other the project to copy the properties to.  Must not be null.
394
     *
395
     * @since phing 2.0
396
     */
397 33
    public function copyInheritedProperties(Project $other)
398
    {
399 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
400 33
    }
401
402
    // ---------------------------------------------------------
403
    //  END Properties methods
404
    // ---------------------------------------------------------
405
406
    /**
407
     * Sets default target
408
     *
409
     * @param string $targetName
410
     */
411 846
    public function setDefaultTarget($targetName)
412
    {
413 846
        $this->defaultTarget = (string) trim($targetName);
414 846
    }
415
416
    /**
417
     * Returns default target
418
     *
419
     * @return string
420
     */
421 33
    public function getDefaultTarget()
422
    {
423 33
        return (string) $this->defaultTarget;
424
    }
425
426
    /**
427
     * Sets the name of the current project
428
     *
429
     * @param string $name name of project
430
     * @return void
431
     * @author Andreas Aderhold, [email protected]
432
     */
433 846
    public function setName($name)
434
    {
435 846
        $this->name = (string) trim($name);
436 846
        $this->setUserProperty("phing.project.name", $this->name);
437 846
    }
438
439
    /**
440
     * Returns the name of this project
441
     *
442
     * @return string projectname
443
     * @author Andreas Aderhold, [email protected]
444
     */
445 2
    public function getName()
446
    {
447 2
        return (string) $this->name;
448
    }
449
450
    /**
451
     * Set the projects description
452
     *
453
     * @param string $description
454
     */
455 6
    public function setDescription($description)
456
    {
457 6
        $this->description = $description;
458 6
    }
459
460
    /**
461
     * return the description, null otherwise
462
     *
463
     * @return string|null
464
     */
465 6
    public function getDescription()
466
    {
467 6
        if ($this->description === null) {
468 5
            $this->description = Description::getAll($this);
469
        }
470 6
        return $this->description;
471
    }
472
473
    /**
474
     * Set the minimum required phing version
475
     *
476
     * @param string $version
477
     */
478 5
    public function setPhingVersion($version)
479
    {
480 5
        $version = str_replace('phing', '', strtolower($version));
481 5
        $this->phingVersion = (string) trim($version);
482 5
    }
483
484
    /**
485
     * Get the minimum required phing version
486
     *
487
     * @return string
488
     */
489 5
    public function getPhingVersion()
490
    {
491 5
        if ($this->phingVersion === null) {
492 5
            $this->setPhingVersion(Phing::getPhingVersion());
493
        }
494
495 5
        return $this->phingVersion;
496
    }
497
498
    /**
499
     * Sets the strict-mode (status) for the current project
500
     * (If strict mode is On, all the warnings would be converted to an error
501
     * (and the build will be stopped/aborted)
502
     *
503
     * @param bool $strictmode
504
     * @return void
505
     * @access public
506
     * @author Utsav Handa, [email protected]
507
     */
508 36
    public function setStrictMode(bool $strictmode)
509
    {
510 36
        $this->strictMode = $strictmode;
511 36
        $this->setProperty("phing.project.strictmode", $this->strictMode);
512 36
    }
513
514
    /**
515
     * Get the strict-mode status for the project
516
     *
517
     * @return boolean
518
     */
519
    public function getStrictmode()
520
    {
521
        return $this->strictMode;
522
    }
523
524
    /**
525
     * Set basedir object from xm
526
     *
527
     * @param File|string $dir
528
     * @throws BuildException
529
     */
530 862
    public function setBasedir($dir)
531
    {
532 862
        if ($dir instanceof File) {
533 96
            $dir = $dir->getAbsolutePath();
534
        }
535
536 862
        $dir = $this->fileUtils->normalize($dir);
537 862
        $dir = FileSystem::getFileSystem()->canonicalize($dir);
538
539 862
        $dir = new File((string) $dir);
540 862
        if (!$dir->exists()) {
541
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " does not exist");
542
        }
543 862
        if (!$dir->isDirectory()) {
544
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " is not a directory");
545
        }
546 862
        $this->basedir = $dir;
547 862
        $this->setPropertyInternal("project.basedir", $this->basedir->getPath());
548 862
        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
549
550
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.
551 862
        chdir($dir->getAbsolutePath());
552 862
    }
553
554
    /**
555
     * Returns the basedir of this project
556
     *
557
     * @return File      Basedir PhingFile object
558
     *
559
     * @throws BuildException
560
     *
561
     * @author Andreas Aderhold, [email protected]
562
     */
563 180
    public function getBasedir()
564
    {
565 180
        if ($this->basedir === null) {
566
            try { // try to set it
567
                $this->setBasedir(".");
568
            } catch (BuildException $exc) {
569
                throw new BuildException("Can not set default basedir. " . $exc->getMessage());
570
            }
571
        }
572
573 180
        return $this->basedir;
574
    }
575
576
    /**
577
     * Set &quot;keep-going&quot; mode. In this mode Ant will try to execute
578
     * as many targets as possible. All targets that do not depend
579
     * on failed target(s) will be executed.  If the keepGoing settor/getter
580
     * methods are used in conjunction with the <code>ant.executor.class</code>
581
     * property, they will have no effect.
582
     *
583
     * @param bool $keepGoingMode &quot;keep-going&quot; mode
584
     */
585 36
    public function setKeepGoingMode($keepGoingMode)
586
    {
587 36
        $this->keepGoingMode = $keepGoingMode;
588 36
    }
589
590
    /**
591
     * Return the keep-going mode.  If the keepGoing settor/getter
592
     * methods are used in conjunction with the <code>phing.executor.class</code>
593
     * property, they will have no effect.
594
     *
595
     * @return bool &quot;keep-going&quot; mode
596
     */
597 36
    public function isKeepGoingMode()
598
    {
599 36
        return $this->keepGoingMode;
600
    }
601
602
    /**
603
     * Sets system properties and the environment variables for this project.
604
     *
605
     * @return void
606
     */
607 846
    public function setSystemProperties()
608
    {
609
610
        // first get system properties
611 846
        $systemP = array_merge($this->getProperties(), Phing::getProperties());
612 846
        foreach ($systemP as $name => $value) {
613 846
            $this->setPropertyInternal($name, $value);
614
        }
615
616
        // and now the env vars
617 846
        foreach ($_SERVER as $name => $value) {
618
            // skip arrays
619 846
            if (is_array($value)) {
620 846
                continue;
621
            }
622 846
            $this->setPropertyInternal('env.' . $name, $value);
623
        }
624 846
    }
625
626
    /**
627
     * Adds a task definition.
628
     *
629
     * @param string $name Name of tag.
630
     * @param string $class The class path to use.
631
     * @param string $classpath The classpat to use.
632
     */
633 73
    public function addTaskDefinition($name, $class, $classpath = null)
634
    {
635 73
        ComponentHelper::getComponentHelper($this)->addTaskDefinition($name, $class, $classpath);
636 73
    }
637
638
    /**
639
     * Returns the task definitions
640
     *
641
     * @return array
642
     */
643 46
    public function getTaskDefinitions()
644
    {
645 46
        return ComponentHelper::getComponentHelper($this)->getTaskDefinitions();
646
    }
647
648
    /**
649
     * Adds a data type definition.
650
     *
651
     * @param string $typeName Name of the type.
652
     * @param string $typeClass The class to use.
653
     * @param string $classpath The classpath to use.
654
     */
655 40
    public function addDataTypeDefinition($typeName, $typeClass, $classpath = null)
656
    {
657 40
        ComponentHelper::getComponentHelper($this)->addDataTypeDefinition($typeName, $typeClass, $classpath);
658 40
    }
659
660
    /**
661
     * Returns the data type definitions
662
     *
663
     * @return array
664
     */
665 847
    public function getDataTypeDefinitions()
666
    {
667 847
        return ComponentHelper::getComponentHelper($this)->getDataTypeDefinitions();
668
    }
669
670
    /**
671
     * Add a new target to the project
672
     *
673
     * @param string $targetName
674
     * @param Target $target
675
     * @throws BuildException
676
     */
677 846
    public function addTarget($targetName, $target)
678
    {
679 846
        if (isset($this->targets[$targetName])) {
680
            throw new BuildException("Duplicate target: $targetName");
681
        }
682 846
        $this->addOrReplaceTarget($targetName, $target);
683 846
    }
684
685
    /**
686
     * Adds or replaces a target in the project
687
     *
688
     * @param string $targetName
689
     * @param Target $target
690
     */
691 846
    public function addOrReplaceTarget($targetName, &$target)
692
    {
693 846
        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
694 846
        $target->setProject($this);
695 846
        $this->targets[$targetName] = $target;
696
697 846
        $ctx = $this->getReference(ProjectConfigurator::PARSING_CONTEXT_REFERENCE);
698 846
        $current = $ctx->getCurrentTargets();
699 846
        $current[$targetName] = $target;
700 846
    }
701
702
    /**
703
     * Returns the available targets
704
     *
705
     * @return Target[]
706
     */
707 846
    public function getTargets()
708
    {
709 846
        return $this->targets;
710
    }
711
712
    /**
713
     * @return string[]
714
     */
715
    public function getExecutedTargetNames()
716
    {
717
        return $this->executedTargetNames;
718
    }
719
720
    /**
721
     * Create a new task instance and return reference to it.
722
     *
723
     * @param string $taskType Task name
724
     * @return Task           A task object
725
     * @throws BuildException
726
     */
727 707
    public function createTask($taskType)
728
    {
729 707
        return ComponentHelper::getComponentHelper($this)->createTask($taskType);
730
    }
731
732
    /**
733
     * Creates a new condition and returns the reference to it
734
     *
735
     * @param string $conditionType
736
     * @return Condition
737
     * @throws BuildException
738
     */
739 1
    public function createCondition($conditionType)
740
    {
741 1
        return ComponentHelper::getComponentHelper($this)->createCondition($conditionType);
742
    }
743
744
    /**
745
     * Create a datatype instance and return reference to it
746
     * See createTask() for explanation how this works
747
     *
748
     * @param string $typeName Type name
749
     * @return object         A datatype object
750
     * @throws BuildException
751
     *                                 Exception
752
     */
753 83
    public function createDataType($typeName)
754
    {
755 83
        return ComponentHelper::getComponentHelper($this)->createDataType($typeName);
756
    }
757
758
    /**
759
     * Executes a list of targets
760
     *
761
     * @param array $targetNames List of target names to execute
762
     * @return void
763
     * @throws BuildException
764
     */
765
    public function executeTargets($targetNames)
766
    {
767
        $this->executedTargetNames = $targetNames;
768
769
        foreach ($targetNames as $tname) {
770
            $this->executeTarget($tname);
771
        }
772
    }
773
774
    /**
775
     * Executes a target
776
     *
777
     * @param string $targetName Name of Target to execute
778
     * @return void
779
     * @throws BuildException
780
     */
781 657
    public function executeTarget($targetName)
782
    {
783
784
        // complain about executing void
785 657
        if ($targetName === null) {
0 ignored issues
show
introduced by
The condition $targetName === null is always false.
Loading history...
786
            throw new BuildException("No target specified");
787
        }
788
789
        // invoke topological sort of the target tree and run all targets
790
        // until targetName occurs.
791 657
        $sortedTargets = $this->topoSort($targetName);
792
793 657
        $curIndex = (int) 0;
794 657
        $curTarget = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $curTarget is dead and can be removed.
Loading history...
795 657
        $thrownException = null;
796 657
        $buildException = null;
797
        do {
798
            try {
799 657
                $curTarget = $sortedTargets[$curIndex++];
800 657
                $curTarget->performTasks();
801 168
            } catch (BuildException $exc) {
802 168
                if (!($this->keepGoingMode)) {
803 168
                    throw $exc;
804
                }
805
                $thrownException = $exc;
806
            }
807 541
            if ($thrownException != null) {
808
                if ($thrownException instanceof BuildException) {
809
                    $this->log(
810
                        "Target '" . $curTarget->getName()
811
                        . "' failed with message '"
812
                        . $thrownException->getMessage() . "'.",
813
                        Project::MSG_ERR
814
                    );
815
                    // only the first build exception is reported
816
                    if ($buildException === null) {
817
                        $buildException = $thrownException;
818
                    }
819
                } else {
820
                    $this->log(
821
                        "Target '" . $curTarget->getName()
822
                        . "' failed with message '"
823
                        . $thrownException->getMessage() . "'." . PHP_EOL
824
                        . $thrownException->getTraceAsString(),
825
                        Project::MSG_ERR
826
                    );
827
                    if ($buildException === null) {
828
                        $buildException = new BuildException($thrownException);
829
                    }
830
                }
831
            }
832 541
        } while ($curTarget->getName() !== $targetName);
833
834 541
        if ($buildException !== null) {
835
            throw $buildException;
836
        }
837 541
    }
838
839
    /**
840
     * Helper function
841
     *
842
     * @param string $fileName
843
     * @param File $rootDir
844
     * @return File
845
     * @throws IOException
846
     */
847 460
    public function resolveFile(string $fileName, File $rootDir = null): File
848
    {
849 460
        if ($rootDir === null) {
850 454
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
851
        }
852 88
        return $this->fileUtils->resolveFile($rootDir, $fileName);
853
    }
854
855
    /**
856
     * Return the boolean equivalent of a string, which is considered
857
     * <code>true</code> if either <code>"on"</code>, <code>"true"</code>,
858
     * or <code>"yes"</code> is found, ignoring case.
859
     *
860
     * @param string $s The string to convert to a boolean value.
861
     *
862
     * @return <code>true</code> if the given string is <code>"on"</code>,
0 ignored issues
show
Documentation Bug introduced by
The doc comment <code>true</code> at position 0 could not be parsed: Unknown type name '<' at position 0 in <code>true</code>.
Loading history...
863
     *         <code>"true"</code> or <code>"yes"</code>, or
864
     *         <code>false</code> otherwise.
865
     */
866 7
    public static function toBoolean($s)
867
    {
868
        return (
869 7
            strcasecmp($s, 'on') === 0
870 7
            || strcasecmp($s, 'true') === 0
871 5
            || strcasecmp($s, 'yes') === 0
872
            // FIXME next condition should be removed if the boolean behavior for properties will be solved
873 7
            || strcasecmp($s, 1) === 0
874
        );
875
    }
876
877
    /**
878
     * Topologically sort a set of Targets.
879
     *
880
     * @param string $rootTarget is the (String) name of the root Target. The sort is
881
     *                         created in such a way that the sequence of Targets until the root
882
     *                         target is the minimum possible such sequence.
883
     * @return Target[] targets in sorted order
884
     * @throws Exception
885
     * @throws BuildException
886
     */
887 657
    public function topoSort($rootTarget)
888
    {
889 657
        $rootTarget = (string) $rootTarget;
890 657
        $ret = [];
891 657
        $state = [];
892 657
        $visiting = [];
893
894
        // We first run a DFS based sort using the root as the starting node.
895
        // This creates the minimum sequence of Targets to the root node.
896
        // We then do a sort on any remaining unVISITED targets.
897
        // This is unnecessary for doing our build, but it catches
898
        // circular dependencies or missing Targets on the entire
899
        // dependency tree, not just on the Targets that depend on the
900
        // build Target.
901
902 657
        $this->tsort($rootTarget, $state, $visiting, $ret);
903
904 657
        $retHuman = "";
905 657
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
906 657
            $retHuman .= (string) $ret[$i] . " ";
907
        }
908 657
        $this->log("Build sequence for target '$rootTarget' is: $retHuman", Project::MSG_VERBOSE);
909
910 657
        $keys = array_keys($this->targets);
911 657
        while ($keys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys of type array 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...
912 657
            $curTargetName = (string) array_shift($keys);
913 657
            if (!isset($state[$curTargetName])) {
914 657
                $st = null;
915
            } else {
916 657
                $st = (string) $state[$curTargetName];
917
            }
918
919 657
            if ($st === null) {
920 657
                $this->tsort($curTargetName, $state, $visiting, $ret);
921 657
            } elseif ($st === "VISITING") {
922
                throw new Exception("Unexpected node in visiting state: $curTargetName");
923
            }
924
        }
925
926 657
        $retHuman = "";
927 657
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
928 657
            $retHuman .= (string) $ret[$i] . " ";
929
        }
930 657
        $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE);
931
932 657
        return $ret;
933
    }
934
935
    // one step in a recursive DFS traversal of the target dependency tree.
936
    // - The array "state" contains the state (VISITED or VISITING or null)
937
    //   of all the target names.
938
    // - The stack "visiting" contains a stack of target names that are
939
    //   currently on the DFS stack. (NB: the target names in "visiting" are
940
    //    exactly the target names in "state" that are in the VISITING state.)
941
    // 1. Set the current target to the VISITING state, and push it onto
942
    //    the "visiting" stack.
943
    // 2. Throw a BuildException if any child of the current node is
944
    //    in the VISITING state (implies there is a cycle.) It uses the
945
    //    "visiting" Stack to construct the cycle.
946
    // 3. If any children have not been VISITED, tsort() the child.
947
    // 4. Add the current target to the Vector "ret" after the children
948
    //    have been visited. Move the current target to the VISITED state.
949
    //    "ret" now contains the sorted sequence of Targets up to the current
950
    //    Target.
951
952
    /**
953
     * @param $root
954
     * @param $state
955
     * @param $visiting
956
     * @param $ret
957
     * @throws BuildException
958
     * @throws Exception
959
     */
960 657
    private function tsort($root, &$state, &$visiting, &$ret)
961
    {
962 657
        $state[$root] = "VISITING";
963 657
        $visiting[] = $root;
964
965 657
        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
966 3
            $target = null;
967
        } else {
968 657
            $target = $this->targets[$root];
969
        }
970
971
        // make sure we exist
972 657
        if ($target === null) {
973 3
            $sb = "Target '$root' does not exist in this project.";
974 3
            array_pop($visiting);
975 3
            if (!empty($visiting)) {
976
                $parent = (string) $visiting[count($visiting) - 1];
977
                $sb .= " It is a dependency of target '$parent'.";
978
            }
979 3
            if ($suggestion = $this->findSuggestion($root)) {
980 2
                $sb .= sprintf(" Did you mean '%s'?", $suggestion);
981
            }
982 3
            throw new BuildException($sb);
983
        }
984
985 657
        $deps = $target->getDependencies();
986
987 657
        while ($deps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deps of type array 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...
988 142
            $cur = (string) array_shift($deps);
989 142
            if (!isset($state[$cur])) {
990 78
                $m = null;
991
            } else {
992 124
                $m = (string) $state[$cur];
993
            }
994 142
            if ($m === null) {
995
                // not been visited
996 78
                $this->tsort($cur, $state, $visiting, $ret);
997 124
            } elseif ($m == "VISITING") {
998
                // currently visiting this node, so have a cycle
999
                throw $this->makeCircularException($cur, $visiting);
1000
            }
1001
        }
1002
1003 657
        $p = (string) array_pop($visiting);
1004 657
        if ($root !== $p) {
1005
            throw new Exception("Unexpected internal error: expected to pop $root but got $p");
1006
        }
1007
1008 657
        $state[$root] = "VISITED";
1009 657
        $ret[] = $target;
1010 657
    }
1011
1012
    /**
1013
     * @param string $end
1014
     * @param array $stk
1015
     * @return BuildException
1016
     */
1017
    private function makeCircularException($end, $stk)
1018
    {
1019
        $sb = "Circular dependency: $end";
1020
        do {
1021
            $c = (string) array_pop($stk);
1022
            $sb .= " <- " . $c;
1023
        } while ($c != $end);
1024
1025
        return new BuildException($sb);
1026
    }
1027
1028
    /**
1029
     * Adds a reference to an object. This method is called when the parser
1030
     * detects a id="foo" attribute. It passes the id as $name and a reference
1031
     * to the object assigned to this id as $value
1032
     *
1033
     * @param string $name
1034
     * @param object $object
1035
     */
1036 866
    public function addReference($name, $object)
1037
    {
1038 866
        $ref = $this->references[$name] ?? null;
1039 866
        if ($ref === $object) {
1040
            return;
1041
        }
1042 866
        if ($ref !== null && !$ref instanceof UnknownElement) {
1043 37
            $this->log("Overriding previous definition of reference to $name", Project::MSG_VERBOSE);
1044
        }
1045 866
        $refName = (is_scalar($object) || $object instanceof PropertyValue) ? (string) $object : get_class($object);
1046 866
        $this->log("Adding reference: $name -> " . $refName, Project::MSG_DEBUG);
1047 866
        $this->references[$name] = $object;
1048 866
    }
1049
1050
    /**
1051
     * Returns the references array.
1052
     *
1053
     * @return array
1054
     */
1055 33
    public function getReferences()
1056
    {
1057 33
        return $this->references;
1058
    }
1059
1060
    /**
1061
     * Returns a specific reference.
1062
     *
1063
     * @param string $key The reference id/key.
1064
     * @return object Reference or null if not defined
1065
     */
1066 867
    public function getReference($key)
1067
    {
1068 867
        return $this->references[$key] ?? null; // just to be explicit
1069
    }
1070
1071
    /**
1072
     * Does the project know this reference?
1073
     *
1074
     * @param string $key The reference id/key.
1075
     * @return bool
1076
     */
1077 5
    public function hasReference(string $key): bool
1078
    {
1079 5
        return isset($this->references[$key]);
1080
    }
1081
1082
    /**
1083
     * Abstracting and simplifyling Logger calls for project messages
1084
     *
1085
     * @param string $msg
1086
     * @param int $level
1087
     */
1088 866
    public function log($msg, $level = Project::MSG_INFO)
1089
    {
1090 866
        $this->logObject($this, $msg, $level);
1091 866
    }
1092
1093
    /**
1094
     * @param mixed $obj
1095
     * @param string $msg
1096
     * @param int $level
1097
     * @param Exception|null $t
1098
     */
1099 869
    public function logObject($obj, $msg, $level, Exception $t = null)
1100
    {
1101 869
        $this->fireMessageLogged($obj, $msg, $level, $t);
1102
1103
        // Checking whether the strict-mode is On, then consider all the warnings
1104
        // as errors.
1105 869
        if (($this->strictMode) && (Project::MSG_WARN == $level)) {
1106
            throw new BuildException('Build contains warnings, considered as errors in strict mode', null);
1107
        }
1108 869
    }
1109
1110
    /**
1111
     * @param BuildListener $listener
1112
     */
1113 846
    public function addBuildListener(BuildListener $listener)
1114
    {
1115 846
        $this->listeners[] = $listener;
1116 846
    }
1117
1118
    /**
1119
     * @param BuildListener $listener
1120
     */
1121 9
    public function removeBuildListener(BuildListener $listener)
1122
    {
1123 9
        $newarray = [];
1124 9
        for ($i = 0, $size = count($this->listeners); $i < $size; $i++) {
1125 9
            if ($this->listeners[$i] !== $listener) {
1126 9
                $newarray[] = $this->listeners[$i];
1127
            }
1128
        }
1129 9
        $this->listeners = $newarray;
1130 9
    }
1131
1132
    /**
1133
     * @return array
1134
     */
1135 33
    public function getBuildListeners()
1136
    {
1137 33
        return $this->listeners;
1138
    }
1139
1140
    public function fireBuildStarted()
1141
    {
1142
        $event = new BuildEvent($this);
1143
        foreach ($this->listeners as $listener) {
1144
            $listener->buildStarted($event);
1145
        }
1146
1147
        $this->log((string) $event, Project::MSG_DEBUG);
1148
    }
1149
1150
    /**
1151
     * @param Exception $exception
1152
     */
1153
    public function fireBuildFinished($exception)
1154
    {
1155
        $event = new BuildEvent($this);
1156
        $event->setException($exception);
1157
        foreach ($this->listeners as $listener) {
1158
            $listener->buildFinished($event);
1159
        }
1160
1161
        $this->log((string) $event, Project::MSG_DEBUG);
1162
    }
1163
1164
    /**
1165
     * @param $target
1166
     */
1167 657
    public function fireTargetStarted($target)
1168
    {
1169 657
        $event = new BuildEvent($target);
1170 657
        foreach ($this->listeners as $listener) {
1171 657
            $listener->targetStarted($event);
1172
        }
1173
1174 657
        $this->log((string) $event, Project::MSG_DEBUG);
1175 657
    }
1176
1177
    /**
1178
     * @param $target
1179
     * @param $exception
1180
     */
1181 657
    public function fireTargetFinished($target, $exception)
1182
    {
1183 657
        $event = new BuildEvent($target);
1184 657
        $event->setException($exception);
1185 657
        foreach ($this->listeners as $listener) {
1186 657
            $listener->targetFinished($event);
1187
        }
1188
1189 657
        $this->log((string) $event, Project::MSG_DEBUG);
1190 657
    }
1191
1192
    /**
1193
     * @param $task
1194
     */
1195 710
    public function fireTaskStarted($task)
1196
    {
1197 710
        $event = new BuildEvent($task);
1198 710
        foreach ($this->listeners as $listener) {
1199 710
            $listener->taskStarted($event);
1200
        }
1201
1202 710
        $this->log((string) $event, Project::MSG_DEBUG);
1203 710
    }
1204
1205
    /**
1206
     * @param $task
1207
     * @param $exception
1208
     */
1209 710
    public function fireTaskFinished($task, $exception)
1210
    {
1211 710
        $event = new BuildEvent($task);
1212 710
        $event->setException($exception);
1213 710
        foreach ($this->listeners as $listener) {
1214 710
            $listener->taskFinished($event);
1215
        }
1216
1217 710
        $this->log((string) $event, Project::MSG_DEBUG);
1218 710
    }
1219
1220
    /**
1221
     * @param $event
1222
     * @param $message
1223
     * @param $priority
1224
     */
1225 869
    public function fireMessageLoggedEvent(BuildEvent $event, $message, $priority)
1226
    {
1227 869
        $event->setMessage($message, $priority);
1228 869
        foreach ($this->listeners as $listener) {
1229 846
            $listener->messageLogged($event);
1230
        }
1231 869
    }
1232
1233
    /**
1234
     * @param mixed $object
1235
     * @param string $message
1236
     * @param int $priority
1237
     * @param Exception $t
1238
     * @throws Exception
1239
     */
1240 869
    public function fireMessageLogged($object, $message, $priority, Exception $t = null)
1241
    {
1242 869
        $event = new BuildEvent($object);
1243 869
        if ($t !== null) {
1244
            $event->setException($t);
1245
        }
1246 869
        $this->fireMessageLoggedEvent($event, $message, $priority);
1247 869
    }
1248
1249
    /**
1250
     * Finds the Target with the most similar name to function's argument
1251
     *
1252
     * Will return null if buildfile has no targets.
1253
     *
1254
     * @see https://www.php.net/manual/en/function.levenshtein.php
1255
     * @param string $unknownTarget Target name
1256
     *
1257
     * @return Target
1258
     */
1259 3
    private function findSuggestion(string $unknownTarget): ?Target
1260
    {
1261 3
        return array_reduce($this->targets, function (?Target $carry, Target $current) use ($unknownTarget): ?Target {
1262
            // Omit target with empty name (there's always one)
1263 3
            if (empty(strval($current))) {
1264 3
                return $carry;
1265
            }
1266
            // $carry is null the first time
1267 2
            if (is_null($carry)) {
1268 2
                return $current;
1269
            }
1270 2
            return levenshtein($unknownTarget, $carry) < levenshtein($unknownTarget, $current) ? $carry : $current;
1271 3
        });
1272
    }
1273
}
1274