Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 284
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
CausedByLines
0.00% covered (danger)
0.00%
0 / 284
0.00% covered (danger)
0.00%
0 / 23
21170
0.00% covered (danger)
0.00%
0 / 1
 emptySingleton
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 withAddedLines
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
240
 asMergedForAssignment
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
182
 asPreservedForParameter
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 asPreservedForArgument
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 asIntersectedWithTaintedness
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 asFilteredForFuncAndParam
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getLinesForGenericReturn
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 withTaintAddedToMethodArgLinks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 forSinkBackprop
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 asAllMaybeMovedAtOffset
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 asAllMovedToKeys
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getForDim
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 asAllCollapsed
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 asAllValueFirstLevel
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 asAllKeyForForeach
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 withOnlyLinks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 asMergedWith
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
650
 getArraySubsetIdx
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 toStringForIssue
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getRelevantLinesForTaintedness
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 isEmpty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toLinesArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toDebugString
n/a
0 / 0
n/a
0 / 0
4
1<?php declare( strict_types=1 );
2
3namespace SecurityCheckPlugin;
4
5use ast\Node;
6use Phan\Language\Element\FunctionInterface;
7
8/**
9 * Value object used to store caused-by lines.
10 */
11class CausedByLines {
12    private const MAX_LINES_PER_ISSUE = 80;
13    // XXX Hack: Enforce a hard limit, or things may explode
14    private const LINES_HARD_LIMIT = 100;
15
16    /**
17     * Note: the links are nullable for performance.
18     * @var array<array<Taintedness|string|MethodLinks|null>>
19     * @phan-var list<array{0:Taintedness,1:string,2:?MethodLinks}>
20     */
21    private $lines = [];
22
23    public static function emptySingleton(): self {
24        static $singleton;
25        if ( !$singleton ) {
26            $singleton = new self();
27        }
28        return $singleton;
29    }
30
31    /**
32     * Adds the given lines to this object. For assignment statements, use {@see self::withAddedAssignmentLines}
33     * @param string[] $lines
34     * @param Taintedness $taintedness
35     * @param MethodLinks|null $links
36     */
37    public function withAddedLines( array $lines, Taintedness $taintedness, ?MethodLinks $links = null ): self {
38        if ( $links && $links->isEmpty() ) {
39            $links = null;
40        }
41        if ( !$links && $taintedness->isSafe() ) {
42            return $this;
43        }
44
45        $ret = new self();
46
47        if ( !$this->lines ) {
48            foreach ( $lines as $line ) {
49                $ret->lines[] = [ $taintedness, $line, $links ];
50            }
51            return $ret;
52        }
53
54        foreach ( $this->lines as $line ) {
55            $ret->lines[] = [ $line[0], $line[1], $line[2] ];
56        }
57
58        foreach ( $lines as $line ) {
59            if ( count( $ret->lines ) >= self::LINES_HARD_LIMIT ) {
60                break;
61            }
62            $idx = array_search( $line, array_column( $ret->lines, 1 ), true );
63            if ( $idx !== false ) {
64                $ret->lines[ $idx ][0] = $ret->lines[ $idx ][0]->asMergedWith( $taintedness );
65                if ( $links && !$ret->lines[$idx][2] ) {
66                    $ret->lines[$idx][2] = $links;
67                } elseif ( $links && $links !== $ret->lines[$idx][2] ) {
68                    $ret->lines[$idx][2] = $ret->lines[$idx][2]->asMergedWith( $links );
69                }
70            } else {
71                $ret->lines[] = [ $taintedness, $line, $links ];
72            }
73        }
74
75        return $ret;
76    }
77
78    /**
79     * Merge caused-by lines from the RHS, and add the given additional lines to this object, as part of an assignment
80     * statement.
81     *
82     * @param CausedByLines $rightLines For the RHS expression
83     * @param string[] $lines
84     * @param Taintedness $taintedness
85     * @param MethodLinks|null $links
86     */
87    public function asMergedForAssignment(
88        self $rightLines,
89        array $lines,
90        Taintedness $taintedness,
91        ?MethodLinks $links = null
92    ): self {
93        if ( $links && $links->isEmpty() ) {
94            $links = null;
95        }
96        if ( !$links && $taintedness->isSafe() ) {
97            return $this->asMergedWith( $rightLines );
98        }
99
100        if ( !$rightLines->lines ) {
101            return $this->withAddedLines( $lines, $taintedness, $links );
102        }
103
104        $ret = $this->withAddedLines( $lines, $taintedness )
105            ->asMergedWith( $rightLines );
106
107        if ( $links ) {
108            $ret = clone $ret;
109            foreach ( $lines as $line ) {
110                if ( count( $ret->lines ) >= self::LINES_HARD_LIMIT ) {
111                    break;
112                }
113
114                $remainingLinks = $links;
115                foreach ( $ret->lines as [ , $lineLine, $lineLinks ] ) {
116                    if ( $lineLine === $line ) {
117                        $remainingLinks = $lineLinks ? $remainingLinks->withoutShape( $lineLinks ) : $remainingLinks;
118                    }
119                }
120                if ( !$remainingLinks->isEmpty() ) {
121                    $ret->lines[] = [ Taintedness::safeSingleton(), $line, $remainingLinks ];
122                }
123            }
124        }
125
126        return $ret;
127    }
128
129    /**
130     * If this object represents the caused-by lines for a given function parameter, apply the effect of a method call
131     * where the argument for that parameter has the specified taintedness and links.
132     */
133    public function asPreservedForParameter(
134        Taintedness $argTaint,
135        MethodLinks $argLinks,
136        FunctionInterface $func,
137        int $param
138    ): self {
139        if ( !$this->lines ) {
140            return $this;
141        }
142        $ret = new self;
143        $argHasLinks = !$argLinks->isEmpty();
144        foreach ( $this->lines as [ $eTaint, $eLine, $eLinks ] ) {
145            if ( $eLinks ) {
146                $preservedTaint = $eLinks->asPreservedTaintednessForFuncParam( $func, $param )
147                    ->asTaintednessForArgument( $argTaint );
148                $newTaint = $eTaint->asMergedWith( $preservedTaint );
149                if ( $argHasLinks || !$newTaint->isSafe() ) {
150                    $ret->lines[] = [ $newTaint, $eLine, $argLinks ];
151                }
152            } else {
153                $ret->lines[] = [ $eTaint, $eLine, $argLinks ];
154            }
155        }
156        return $ret;
157    }
158
159    /**
160     * If this object represents the caused-by lines for a given function argument, apply the effect of a method call
161     * that preserves the given taintedness.
162     */
163    public function asPreservedForArgument(
164        PreservedTaintedness $preservedTaint
165    ): self {
166        if ( !$this->lines ) {
167            return $this;
168        }
169        $ret = new self;
170        foreach ( $this->lines as [ $eTaint, $eLine, ] ) {
171            $newTaint = $preservedTaint->asTaintednessForArgument( $eTaint );
172            // TODO: Pass appropriate links through, see I1bd8ae302e91a2b6b951953bc321ea6ae89d5955
173            $newLinks = null;
174            if ( !$newTaint->isSafe() ) {
175                $ret->lines[] = [ $newTaint, $eLine, $newLinks ];
176            }
177        }
178        return $ret;
179    }
180
181    /**
182     * @param Taintedness $taintedness
183     * @todo Migrate callers to asPreservedForArgument and drop this.
184     */
185    public function asIntersectedWithTaintedness( Taintedness $taintedness ): self {
186        if ( !$this->lines ) {
187            return $this;
188        }
189        $ret = new self;
190        $curTaint = $taintedness->get();
191        foreach ( $this->lines as [ $eTaint, $eLine, $links ] ) {
192            $newTaint = $curTaint !== SecurityCheckPlugin::NO_TAINT
193                ? $eTaint->withOnly( $curTaint )
194                : Taintedness::safeSingleton();
195            $ret->lines[] = [ $newTaint, $eLine, $links ];
196        }
197        return $ret;
198    }
199
200    public function asFilteredForFuncAndParam( FunctionInterface $func, int $param ): self {
201        if ( !$this->lines ) {
202            return $this;
203        }
204        $ret = new self;
205        $safeTaint = Taintedness::safeSingleton();
206        foreach ( $this->lines as [ , $lineLine, $lineLinks ] ) {
207            if ( $lineLinks && $lineLinks->hasDataForFuncAndParam( $func, $param ) ) {
208                $ret->lines[] = [ $safeTaint, $lineLine, $lineLinks ];
209            }
210        }
211        return $ret;
212    }
213
214    public function getLinesForGenericReturn(): self {
215        if ( !$this->lines ) {
216            return $this;
217        }
218        $ret = new self;
219        foreach ( $this->lines as [ $lineTaint, $lineLine, ] ) {
220            if ( !$lineTaint->isSafe() ) {
221                // For generic lines, links don't matter
222                $ret->lines[] = [ $lineTaint, $lineLine, null ];
223            }
224        }
225        return $ret;
226    }
227
228    /**
229     * For every line in this object, check if the line has links for $func, and if so, add preserved taintedness from
230     * $taintedness to the line.
231     *
232     * @param Taintedness $taintedness
233     * @param FunctionInterface $func
234     * @param int $i Parameter index
235     * @param bool $isSink True when backpropagating method links for a sink (and $taintedness is the taintedness of the
236     * sink); false when backpropagating variable links (and $taintedness is the new taintedness of the variable).
237     */
238    public function withTaintAddedToMethodArgLinks(
239        Taintedness $taintedness,
240        FunctionInterface $func,
241        int $i,
242        bool $isSink
243    ): self {
244        if ( !$this->lines ) {
245            return $this;
246        }
247        $ret = new self;
248        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
249            if ( $lineLinks && $lineLinks->hasDataForFuncAndParam( $func, $i ) ) {
250                $preservedTaint = $lineLinks->asPreservedTaintednessForFuncParam( $func, $i );
251                $newTaint = $isSink
252                    ? $preservedTaint->asTaintednessForBackpropError( $taintedness )
253                    : $preservedTaint->asTaintednessForVarBackpropError( $taintedness );
254                $ret->lines[] = [ $lineTaint->asMergedWith( $newTaint ), $lineLine, $lineLinks ];
255            } else {
256                $ret->lines[] = [ $lineTaint, $lineLine, $lineLinks ];
257            }
258        }
259        return $ret;
260    }
261
262    public function forSinkBackprop( MethodLinks $links, FunctionInterface $func, int $param ): self {
263        if ( !$this->lines ) {
264            return $this;
265        }
266        $ret = new self;
267        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
268            $newTaint = $lineTaint->asYesToExecTaint()->appliedToLinksForBackprop( $links, $func, $param );
269            if ( !$newTaint->isSafe() ) {
270                $ret->lines[] = [ $newTaint->asExecToYesTaint(), $lineLine, $lineLinks ];
271            }
272        }
273        return $ret;
274    }
275
276    /**
277     * Returns a copy of $this with all taintedness and links moved at the given offset.
278     * @param Node|mixed $offset
279     */
280    public function asAllMaybeMovedAtOffset( mixed $offset ): self {
281        if ( !$this->lines ) {
282            return $this;
283        }
284        $ret = new self;
285        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
286            $ret->lines[] = [
287                $lineTaint->asMaybeMovedAtOffset( $offset ),
288                $lineLine,
289                $lineLinks ? $lineLinks->asMaybeMovedAtOffset( $offset ) : null
290            ];
291        }
292        return $ret;
293    }
294
295    /**
296     * Returns a copy of $this with all taintedness and links moved inside keys.
297     */
298    public function asAllMovedToKeys(): self {
299        if ( !$this->lines ) {
300            return $this;
301        }
302        $ret = new self;
303        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
304            $ret->lines[] = [
305                $lineTaint->asMovedToKeys(),
306                $lineLine,
307                $lineLinks ? $lineLinks->asMovedToKeys() : null
308            ];
309        }
310        return $ret;
311    }
312
313    /**
314     * @param Node|mixed $dim
315     * @param bool $pushOffsetsInLinks
316     */
317    public function getForDim( mixed $dim, bool $pushOffsetsInLinks = true ): self {
318        if ( !$this->lines ) {
319            return $this;
320        }
321        $ret = new self;
322        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
323            $newTaint = $lineTaint->getTaintednessForOffsetOrWhole( $dim );
324            $newLinks = $lineLinks ? $lineLinks->getForDim( $dim, $pushOffsetsInLinks ) : null;
325            if ( $newLinks && $newLinks->isEmpty() ) {
326                $newLinks = null;
327            }
328            if ( $newLinks || !$newTaint->isSafe() ) {
329                $ret->lines[] = [
330                    $newTaint,
331                    $lineLine,
332                    $newLinks
333                ];
334            }
335        }
336        return $ret;
337    }
338
339    public function asAllCollapsed(): self {
340        if ( !$this->lines ) {
341            return $this;
342        }
343        $ret = new self;
344        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
345            $ret->lines[] = [
346                $lineTaint->asCollapsed(),
347                $lineLine,
348                $lineLinks ? $lineLinks->asCollapsed() : null
349            ];
350        }
351        return $ret;
352    }
353
354    public function asAllValueFirstLevel(): self {
355        if ( !$this->lines ) {
356            return $this;
357        }
358        $ret = new self;
359        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
360            $ret->lines[] = [
361                $lineTaint->asValueFirstLevel(),
362                $lineLine,
363                $lineLinks ? $lineLinks->asValueFirstLevel() : null
364            ];
365        }
366        return $ret;
367    }
368
369    public function asAllKeyForForeach(): self {
370        if ( !$this->lines ) {
371            return $this;
372        }
373        $ret = new self;
374        foreach ( $this->lines as [ $lineTaint, $lineLine, $lineLinks ] ) {
375            $newTaint = $lineTaint->asKeyForForeach();
376            $newLinks = $lineLinks ? $lineLinks->asKeyForForeach() : null;
377            if ( $newLinks && $newLinks->isEmpty() ) {
378                $newLinks = null;
379            }
380            if ( $newLinks || !$newTaint->isSafe() ) {
381                $ret->lines[] = [ $newTaint, $lineLine, $newLinks ];
382            }
383        }
384        return $ret;
385    }
386
387    public function withOnlyLinks(): self {
388        if ( !$this->lines ) {
389            return $this;
390        }
391        $ret = new self;
392        $safeTaint = Taintedness::safeSingleton();
393        foreach ( $this->lines as [ , $lineLine, $lineLinks ] ) {
394            if ( $lineLinks && !$lineLinks->isEmpty() ) {
395                $ret->lines[] = [ $safeTaint, $lineLine, $lineLinks ];
396            }
397        }
398        return $ret;
399    }
400
401    /**
402     * @note this isn't a merge operation like array_merge. What this method does is:
403     * 1 - if $other is a subset of $this, leave $this as-is;
404     * 2 - update taintedness values in $this if the *lines* (not taint values) in $other
405     *   are a subset of the lines in $this;
406     * 3 - if an upper set of $this *lines* is also a lower set of $other *lines*, remove that upper
407     *   set from $this and merge the rest with $other;
408     * 4 - array_merge otherwise;
409     *
410     * Step 2 is very important, because otherwise, caused-by lines can grow exponentially if
411     * even a single taintedness value in $this changes.
412     *
413     * @param self $other
414     * @param int $dimDepth Only used for assignments; depth of the array index access on the LHS.
415     */
416    public function asMergedWith( self $other, int $dimDepth = 0 ): self {
417        $emptySingleton = self::emptySingleton();
418        if ( $this === $emptySingleton ) {
419            return $other;
420        }
421        if ( $other === $emptySingleton ) {
422            return $this;
423        }
424
425        $ret = clone $this;
426
427        if ( !$ret->lines ) {
428            $ret->lines = $other->lines;
429            return $ret;
430        }
431        if ( !$other->lines || self::getArraySubsetIdx( $ret->lines, $other->lines ) !== false ) {
432            return $ret;
433        }
434
435        $baseLines = array_column( $ret->lines, 1 );
436        $newLines = array_column( $other->lines, 1 );
437        $subsIdx = self::getArraySubsetIdx( $baseLines, $newLines );
438
439        if ( $subsIdx === false ) {
440            // Try reversing the order to see if we get a better merge.
441            // TODO This whole thing is horrible. We need a better way to merge caused-by lines programmatically
442            $reverseSubsetIdx = self::getArraySubsetIdx( $newLines, $baseLines );
443            if ( $reverseSubsetIdx !== false ) {
444                [ $ret, $other ] = [ clone $other, $ret ];
445                [ $baseLines, $newLines ] = [ $newLines, $baseLines ];
446                $subsIdx = $reverseSubsetIdx;
447            }
448        }
449
450        if ( $subsIdx !== false ) {
451            foreach ( $other->lines as $i => $otherLine ) {
452                /** @var Taintedness $curTaint */
453                $curTaint = $ret->lines[ $i + $subsIdx ][0];
454                $ret->lines[ $i + $subsIdx ][0] = $dimDepth
455                    ? $curTaint->asMergedForAssignment( $otherLine[0], $dimDepth )
456                    : $curTaint->asMergedWith( $otherLine[0] );
457                /** @var MethodLinks $curLinks */
458                $curLinks = $ret->lines[ $i + $subsIdx ][2];
459                $otherLinks = $otherLine[2];
460                if ( $otherLinks && !$curLinks ) {
461                    $ret->lines[$i + $subsIdx][2] = $otherLinks;
462                } elseif ( $otherLinks && $otherLinks !== $curLinks ) {
463                    $ret->lines[$i + $subsIdx][2] = $dimDepth
464                        ? $curLinks->asMergedForAssignment( $otherLinks, $dimDepth )
465                        : $curLinks->asMergedWith( $otherLinks );
466                }
467            }
468            return $ret;
469        }
470
471        $resultingLines = null;
472        $baseLen = count( $ret->lines );
473        $newLen = count( $other->lines );
474        // NOTE: array_shift is O(n), and O(n^2) over all iterations, because it reindexes the whole array.
475        // So reverse the arrays, that is O(n) twice, and use array_pop which is O(1) (O(n) for all iterations)
476        $remaining = array_reverse( $baseLines );
477        $newRev = array_reverse( $newLines );
478        // Assuming the lines as posets with the "natural" order used by PHP (that is, not the keys):
479        // since we're working with reversed arrays, remaining lines should be an upper set of the reversed
480        // new lines; which is to say, a lower set of the non-reversed new lines.
481        $expectedIndex = $newLen - $baseLen;
482        do {
483            if ( $expectedIndex >= 0 && self::getArraySubsetIdx( $newRev, $remaining ) === $expectedIndex ) {
484                $startIdx = $baseLen - $newLen + $expectedIndex;
485                for ( $j = $startIdx; $j < $baseLen; $j++ ) {
486                    /** @var Taintedness $curTaint */
487                    $curTaint = $ret->lines[$j][0];
488                    $otherTaint = $other->lines[$j - $startIdx][0];
489                    $ret->lines[$j][0] = $dimDepth
490                        ? $curTaint->asMergedForAssignment( $otherTaint, $dimDepth )
491                        : $curTaint->asMergedWith( $otherTaint );
492                    $secondLinks = $other->lines[$j - $startIdx][2];
493                    /** @var MethodLinks $curLinks */
494                    $curLinks = $ret->lines[$j][2];
495                    if ( $secondLinks && !$curLinks ) {
496                        $ret->lines[$j][2] = $secondLinks;
497                    } elseif ( $secondLinks && $secondLinks !== $curLinks ) {
498                        $ret->lines[$j][2] = $dimDepth
499                            ? $curLinks->asMergedForAssignment( $secondLinks, $dimDepth )
500                            : $curLinks->asMergedWith( $secondLinks );
501                    }
502                }
503                $resultingLines = array_merge( $ret->lines, array_slice( $other->lines, $newLen - $expectedIndex ) );
504                break;
505            }
506            array_pop( $remaining );
507            $expectedIndex++;
508        } while ( $remaining );
509        $resultingLines ??= array_merge( $ret->lines, $other->lines );
510
511        $ret->lines = array_slice( $resultingLines, 0, self::LINES_HARD_LIMIT );
512
513        return $ret;
514    }
515
516    /**
517     * Check whether $needle is subset of $haystack, regardless of the keys, and returns
518     * the starting index of the subset in the $haystack array. If the subset occurs multiple
519     * times, this will just find the first one.
520     *
521     * @param array[] $haystack
522     * @phan-param list<array{0:Taintedness,1:string,2:?MethodLinks}> $haystack
523     * @param array[] $needle
524     * @phan-param list<array{0:Taintedness,1:string,2:?MethodLinks}> $needle
525     * @return false|int False if not a subset, the starting index if it is.
526     * @note Use strict comparisons with the return value!
527     */
528    private static function getArraySubsetIdx( array $haystack, array $needle ): bool|int {
529        if ( !$needle || !$haystack ) {
530            // For our needs, the empty array is not a subset of anything
531            return false;
532        }
533
534        $needleLength = count( $needle );
535        $haystackLength = count( $haystack );
536        if ( $haystackLength < $needleLength ) {
537            return false;
538        }
539        $curIdx = 0;
540        foreach ( $haystack as $i => $el ) {
541            if ( $el === $needle[ $curIdx ] ) {
542                $curIdx++;
543            } else {
544                $curIdx = 0;
545            }
546            if ( $curIdx === $needleLength ) {
547                return $i - ( $needleLength - 1 );
548            }
549        }
550        return false;
551    }
552
553    /**
554     * Return a truncated, stringified representation of these lines to be used when reporting issues.
555     *
556     * @todo Perhaps this should include the first and last X lines, not the first 2X. However,
557     *   doing so would make phan emit a new issue for the same line whenever new caused-by
558     *   lines are added to the array.
559     *
560     * @param Taintedness $sinkTaint Must have EXEC flags only.
561     * @param Taintedness $exprTaint Must have normal flags only.
562     * @param bool $isSinkError Whether this object refers to a sink (and not the expr)
563     */
564    public function toStringForIssue( Taintedness $sinkTaint, Taintedness $exprTaint, bool $isSinkError ): string {
565        $filteredLines = $this->getRelevantLinesForTaintedness( $sinkTaint, $exprTaint, $isSinkError );
566        if ( !$filteredLines ) {
567            return '';
568        }
569
570        if ( count( $filteredLines ) <= self::MAX_LINES_PER_ISSUE ) {
571            $linesPart = implode( '; ', $filteredLines );
572        } else {
573            $linesPart = implode( '; ', array_slice( $filteredLines, 0, self::MAX_LINES_PER_ISSUE ) ) . '; ...';
574        }
575        return ' (Caused by: ' . $linesPart . ')';
576    }
577
578    /**
579     * @param Taintedness $sinkTaint With EXEC flags only.
580     * @param Taintedness $exprTaint With normal flags only.
581     * @param bool $isSinkError
582     * @return string[]
583     */
584    private function getRelevantLinesForTaintedness(
585        Taintedness $sinkTaint,
586        Taintedness $exprTaint,
587        bool $isSinkError
588    ): array {
589        $ret = [];
590        foreach ( $this->lines as [ $lineTaint, $lineText ] ) {
591            $intersection = $isSinkError
592                ? Taintedness::intersectForSink( $lineTaint->asYesToExecTaint(), $exprTaint )
593                : Taintedness::intersectForSink( $sinkTaint, $lineTaint );
594            if ( !$intersection->isSafe() ) {
595                $ret[] = $lineText;
596            }
597        }
598        return $ret;
599    }
600
601    public function isEmpty(): bool {
602        return $this->lines === [];
603    }
604
605    /**
606     * @return string[]
607     * @suppress PhanUnreferencedPublicMethod
608     */
609    public function toLinesArray(): array {
610        return array_column( $this->lines, 1 );
611    }
612
613    /**
614     * @suppress PhanUnreferencedPublicMethod
615     * @codeCoverageIgnore
616     */
617    public function toDebugString(): string {
618        if ( $this === self::emptySingleton() ) {
619            return '(empty)';
620        }
621        $r = [];
622        foreach ( $this->lines as [ $t, $line, $links ] ) {
623            $r[] = "\t[\n\t\tT: " . $t->toShortString() . "\n\t\tL: " . $line . "\n\t\tLinks: " .
624                ( $links ? $links->toString( "\t\t" ) : 'none' ) . "\n\t]";
625        }
626        return "[\n" . implode( ",\n", $r ) . "\n]";
627    }
628}