Project::getStrictmode()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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