Completed
Push — master ( 9026dd...ccfcc7 )
by Colin
15s
created

DelimiterStack::processDelimiters()   C

Complexity

Conditions 15
Paths 30

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 15

Importance

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