Completed
Push — master ( b42489...8df37e )
by Colin
14s queued 11s
created

src/Delimiter/DelimiterStack.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
12
 *  - (c) Atlassian Pty Ltd
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace League\CommonMark\Delimiter;
19
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
21
use League\CommonMark\Inline\AdjacentTextMerger;
22
23
final class DelimiterStack
24
{
25
    /**
26
     * @var DelimiterInterface|null
27
     */
28
    private $top;
29
30 945
    public function push(DelimiterInterface $newDelimiter)
31
    {
32 945
        $newDelimiter->setPrevious($this->top);
33
34 945
        if ($this->top !== null) {
35 522
            $this->top->setNext($newDelimiter);
36
        }
37
38 945
        $this->top = $newDelimiter;
39 945
    }
40
41
    /**
42
     * @param DelimiterInterface|null $stackBottom
43
     *
44
     * @return DelimiterInterface|null
45
     */
46 2028
    private function findEarliest(DelimiterInterface $stackBottom = null): ?DelimiterInterface
47
    {
48 2028
        $delimiter = $this->top;
49 2028
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
50 480
            $delimiter = $delimiter->getPrevious();
51
        }
52
53 2028
        return $delimiter;
54
    }
55
56
    /**
57
     * @param DelimiterInterface $delimiter
58
     */
59 945
    public function removeDelimiter(DelimiterInterface $delimiter)
60
    {
61 945
        if ($delimiter->getPrevious() !== null) {
62 282
            $delimiter->getPrevious()->setNext($delimiter->getNext());
63
        }
64
65 945
        if ($delimiter->getNext() === null) {
66
            // top of stack
67 945
            $this->top = $delimiter->getPrevious();
68
        } else {
69 411
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
70
        }
71 945
    }
72
73 417
    private function removeDelimiterAndNode(DelimiterInterface $delimiter)
74
    {
75 417
        $delimiter->getInlineNode()->detach();
76 417
        $this->removeDelimiter($delimiter);
77 417
    }
78
79 417
    private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer)
80
    {
81 417
        $delimiter = $closer->getPrevious();
82 417
        while ($delimiter !== null && $delimiter !== $opener) {
83 21
            $previous = $delimiter->getPrevious();
84 21
            $this->removeDelimiter($delimiter);
85 21
            $delimiter = $previous;
86
        }
87 417
    }
88
89
    /**
90
     * @param DelimiterInterface|null $stackBottom
91
     */
92 2028
    public function removeAll(DelimiterInterface $stackBottom = null)
93
    {
94 2028
        while ($this->top && $this->top !== $stackBottom) {
95 489
            $this->removeDelimiter($this->top);
96
        }
97 2028
    }
98
99
    /**
100
     * @param string $character
101
     */
102 294
    public function removeEarlierMatches(string $character)
103
    {
104 294
        $opener = $this->top;
105 294
        while ($opener !== null) {
106 54
            if ($opener->getChar() === $character) {
107 24
                $opener->setActive(false);
108
            }
109
110 54
            $opener = $opener->getPrevious();
111
        }
112 294
    }
113
114
    /**
115
     * @param string|string[] $characters
116
     *
117
     * @return DelimiterInterface|null
118
     */
119 435
    public function searchByCharacter($characters): ?DelimiterInterface
120
    {
121 435
        if (!\is_array($characters)) {
122
            $characters = [$characters];
123
        }
124
125 435
        $opener = $this->top;
126 435
        while ($opener !== null) {
127 429
            if (\in_array($opener->getChar(), $characters)) {
128 426
                break;
129
            }
130 72
            $opener = $opener->getPrevious();
131
        }
132
133 435
        return $opener;
134
    }
135
136 2028
    public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors)
137
    {
138 2028
        $openersBottom = [];
139
140
        // Find first closer above stackBottom
141 2028
        $closer = $this->findEarliest($stackBottom);
142
143
        // Move forward, looking for closers, and handling each
144 2028
        while ($closer !== null) {
145 873
            $delimiterChar = $closer->getChar();
146
147 873
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
148 873
            if (!$closer->canClose() || $delimiterProcessor === null) {
149 795
                $closer = $closer->getNext();
150 795
                continue;
151
            }
152
153 498
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
154
155 498
            $useDelims = 0;
156 498
            $openerFound = false;
157 498
            $potentialOpenerFound = false;
158 498
            $opener = $closer->getPrevious();
159 498
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
160 441
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
161 420
                    $potentialOpenerFound = true;
162 420
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
163 420
                    if ($useDelims > 0) {
164 417
                        $openerFound = true;
165 417
                        break;
166
                    }
167
                }
168 54
                $opener = $opener->getPrevious();
169
            }
170
171 498
            if (!$openerFound) {
172 153
                if (!$potentialOpenerFound) {
173
                    // Only do this when we didn't even have a potential
174
                    // opener (one that matches the character and can open).
175
                    // If an opener was rejected because of the number of
176
                    // delimiters (e.g. because of the "multiple of 3"
177
                    // Set lower bound for future searches for openersrule),
178
                    // we want to consider it next time because the number
179
                    // of delimiters can change as we continue processing.
180 138
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
181 138
                    if (!$closer->canOpen()) {
182
                        // We can remove a closer that can't be an opener,
183
                        // once we've seen there's no matching opener.
184 96
                        $this->removeDelimiter($closer);
185
                    }
186
                }
187 153
                $closer = $closer->getNext();
188 153
                continue;
189
            }
190
191 417
            $openerNode = $opener->getInlineNode();
192 417
            $closerNode = $closer->getInlineNode();
193
194
            // Remove number of used delimiters from stack and inline nodes.
195 417
            $opener->setLength($opener->getLength() - $useDelims);
196 417
            $closer->setLength($closer->getLength() - $useDelims);
197
198 417
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
199 417
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
200
201 417
            $this->removeDelimitersBetween($opener, $closer);
0 ignored issues
show
It seems like $opener can be null; however, removeDelimitersBetween() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
202
            // The delimiter processor can re-parent the nodes between opener and closer,
203
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
204 417
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
205 417
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
206
207
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
208 417
            if ($opener->getLength() === 0) {
209 393
                $this->removeDelimiterAndNode($opener);
0 ignored issues
show
It seems like $opener can be null; however, removeDelimiterAndNode() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
210
            }
211
212 417
            if ($closer->getLength() === 0) {
213 393
                $next = $closer->getNext();
214 393
                $this->removeDelimiterAndNode($closer);
215 393
                $closer = $next;
216
            }
217
        }
218
219
        // Remove all delimiters
220 2028
        $this->removeAll($stackBottom);
221 2028
    }
222
}
223