Completed
Push — master ( ccfcc7...585e6a )
by Colin
09:20 queued 03:25
created

DelimiterStack::processDelimiters()   C

Complexity

Conditions 15
Paths 30

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 15

Importance

Changes 0
Metric Value
dl 0
loc 86
ccs 46
cts 46
cp 1
rs 5.0387
c 0
b 0
f 0
cc 15
nc 30
nop 2
crap 15

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
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
class DelimiterStack
24
{
25
    /**
26
     * @var Delimiter|null
27
     */
28
    private $top;
29
30 996
    public function push(Delimiter $newDelimiter)
31
    {
32 996
        $newDelimiter->setPrevious($this->top);
33
34 996
        if ($this->top !== null) {
35 597
            $this->top->setNext($newDelimiter);
36
        }
37
38 996
        $this->top = $newDelimiter;
39 996
    }
40
41
    /**
42
     * @param Delimiter|null $stackBottom
43
     *
44
     * @return Delimiter|null
45
     */
46 2025
    private function findEarliest(Delimiter $stackBottom = null): ?Delimiter
47
    {
48 2025
        $delimiter = $this->top;
49 2025
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
50 555
            $delimiter = $delimiter->getPrevious();
51
        }
52
53 2025
        return $delimiter;
54
    }
55
56
    /**
57
     * @param Delimiter $delimiter
58
     */
59 996
    public function removeDelimiter(Delimiter $delimiter)
60
    {
61 996
        if ($delimiter->getPrevious() !== null) {
62 366
            $delimiter->getPrevious()->setNext($delimiter->getNext());
63
        }
64
65 996
        if ($delimiter->getNext() === null) {
66
            // top of stack
67 996
            $this->top = $delimiter->getPrevious();
68
        } else {
69 408
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
70
        }
71 996
    }
72
73 414
    private function removeDelimiterAndNode(Delimiter $delimiter)
74
    {
75 414
        $delimiter->getInlineNode()->detach();
76 414
        $this->removeDelimiter($delimiter);
77 414
    }
78
79 414
    private function removeDelimitersBetween(Delimiter $opener, Delimiter $closer)
80
    {
81 414
        $delimiter = $closer->getPrevious();
82 414
        while ($delimiter !== null && $delimiter !== $opener) {
83 27
            $previous = $delimiter->getPrevious();
84 27
            $this->removeDelimiter($delimiter);
85 27
            $delimiter = $previous;
86
        }
87 414
    }
88
89
    /**
90
     * @param Delimiter|null $stackBottom
91
     */
92 2025
    public function removeAll(Delimiter $stackBottom = null)
93
    {
94 2025
        while ($this->top && $this->top !== $stackBottom) {
95 573
            $this->removeDelimiter($this->top);
96
        }
97 2025
    }
98
99
    /**
100
     * @param string $character
101
     */
102 297
    public function removeEarlierMatches(string $character)
103
    {
104 297
        $opener = $this->top;
105 297
        while ($opener !== null) {
106 54
            if ($opener->getChar() === $character) {
107 24
                $opener->setActive(false);
108
            }
109
110 54
            $opener = $opener->getPrevious();
111
        }
112 297
    }
113
114
    /**
115
     * @param string|string[] $characters
116
     *
117
     * @return Delimiter|null
118
     */
119 438
    public function searchByCharacter($characters): ?Delimiter
120
    {
121 438
        if (!\is_array($characters)) {
122
            $characters = [$characters];
123
        }
124
125 438
        $opener = $this->top;
126 438
        while ($opener !== null) {
127 432
            if (\in_array($opener->getChar(), $characters)) {
128 429
                break;
129
            }
130 72
            $opener = $opener->getPrevious();
131
        }
132
133 438
        return $opener;
134
    }
135
136 2025
    public function processDelimiters(?Delimiter $stackBottom, DelimiterProcessorCollection $processors)
137
    {
138 2025
        $openersBottom = [];
139
140
        // Find first closer above stackBottom
141 2025
        $closer = $this->findEarliest($stackBottom);
142
143
        // Move forward, looking for closers, and handling each
144 2025
        while ($closer !== null) {
145 924
            $delimiterChar = $closer->getChar();
146
147 924
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
148 924
            if (!$closer->canClose() || $delimiterProcessor === null) {
149 876
                $closer = $closer->getNext();
150 876
                continue;
151
            }
152
153 495
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
154
155 495
            $useDelims = 0;
156 495
            $openerFound = false;
157 495
            $potentialOpenerFound = false;
158 495
            $opener = $closer->getPrevious();
159 495
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
160 468
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
161 417
                    $potentialOpenerFound = true;
162 417
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
163 417
                    if ($useDelims > 0) {
164 414
                        $openerFound = true;
165 414
                        break;
166
                    }
167
                }
168 90
                $opener = $opener->getPrevious();
169
            }
170
171 495
            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 414
            $openerNode = $opener->getInlineNode();
192 414
            $closerNode = $closer->getInlineNode();
193
194
            // Remove number of used delimiters from stack and inline nodes.
195 414
            $opener->setNumDelims($opener->getNumDelims() - $useDelims);
196 414
            $closer->setNumDelims($closer->getNumDelims() - $useDelims);
197
198 414
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
199 414
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
200
201 414
            $this->removeDelimitersBetween($opener, $closer);
0 ignored issues
show
Bug introduced by
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 414
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
205 414
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
206
207
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
208 414
            if ($opener->getNumDelims() === 0) {
209 390
                $this->removeDelimiterAndNode($opener);
0 ignored issues
show
Bug introduced by
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 414
            if ($closer->getNumDelims() === 0) {
213 390
                $next = $closer->getNext();
214 390
                $this->removeDelimiterAndNode($closer);
215 390
                $closer = $next;
216
            }
217
        }
218
219
        // Remove all delimiters
220 2025
        $this->removeAll($stackBottom);
221 2025
    }
222
}
223