Completed
Push — delimiter ( f748e4 )
by Colin
03:30
created

DelimiterStack::processDelimiters()   C

Complexity

Conditions 15
Paths 30

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 86
rs 5.0387
c 0
b 0
f 0
cc 15
nc 30
nop 2

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