Passed
Push — master ( 2d0497...67acf7 )
by Siad
05:26
created

Project::findSuggestion()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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