DelimiterStack   B
last analyzed

Coupling/Cohesion

Components 1
Dependencies 1

Complexity

Total Complexity 42

Size/Duplication

Total Lines 193
Duplicated Lines 0 %

Test Coverage

Coverage 84.21%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 42
c 1
b 0
f 0
lcom 1
cbo 1
dl 0
loc 193
ccs 80
cts 95
cp 0.8421
rs 8.295

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getTop() 0 4 1
A push() 0 10 2
A findEarliest() 0 9 3
A removeDelimiter() 0 13 3
A removeAll() 0 6 3
A removeEarlierMatches() 0 11 3
A searchByCharacter() 0 16 4
B findFirstMatchingOpener() 0 13 6
C iterateByCharacters() 0 39 8
B findMatchingOpener() 0 14 9

How to fix   Complexity   

Complex Class

Complex classes like DelimiterStack often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DelimiterStack, and based on these observations, apply Extract Interface, too.

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
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark\Delimiter;
16
17
class DelimiterStack
18
{
19
    /**
20
     * @var Delimiter|null
21
     */
22
    protected $top;
23
24
    public function getTop()
25
    {
26
        return $this->top;
27
    }
28
29 864
    public function push(Delimiter $newDelimiter)
30
    {
31 864
        $newDelimiter->setPrevious($this->top);
32
33 864
        if ($this->top !== null) {
34 507
            $this->top->setNext($newDelimiter);
35 507
        }
36
37 864
        $this->top = $newDelimiter;
38 864
    }
39
40
    /**
41
     * @param Delimiter|null $stackBottom
42
     *
43
     * @return Delimiter|null
44
     */
45 1656
    public function findEarliest(Delimiter $stackBottom = null)
46
    {
47 1656
        $delimiter = $this->top;
48 1656
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
49 465
            $delimiter = $delimiter->getPrevious();
50 465
        }
51
52 1656
        return $delimiter;
53
    }
54
55
    /**
56
     * @param Delimiter $delimiter
57
     */
58 864
    public function removeDelimiter(Delimiter $delimiter)
59
    {
60 864
        if ($delimiter->getPrevious() !== null) {
61 342
            $delimiter->getPrevious()->setNext($delimiter->getNext());
62 342
        }
63
64 864
        if ($delimiter->getNext() === null) {
65
            // top of stack
66 864
            $this->top = $delimiter->getPrevious();
67 864
        } else {
68 333
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
69
        }
70 864
    }
71
72
    /**
73
     * @param Delimiter|null $stackBottom
74
     */
75 1656
    public function removeAll(Delimiter $stackBottom = null)
76
    {
77 1656
        while ($this->top && $this->top !== $stackBottom) {
78 528
            $this->removeDelimiter($this->top);
79 528
        }
80 1656
    }
81
82
    /**
83
     * @param string $character
84
     */
85 279
    public function removeEarlierMatches($character)
86
    {
87 279
        $opener = $this->top;
88 279
        while ($opener !== null) {
89 54
            if ($opener->getChar() === $character) {
90 24
                $opener->setActive(false);
91 24
            }
92
93 54
            $opener = $opener->getPrevious();
94 54
        }
95 279
    }
96
97
    /**
98
     * @param string|string[] $characters
99
     *
100
     * @return Delimiter|null
101
     */
102 405
    public function searchByCharacter($characters)
103
    {
104 405
        if (!is_array($characters)) {
105
            $characters = [$characters];
106
        }
107
108 405
        $opener = $this->top;
109 405
        while ($opener !== null) {
110 399
            if (in_array($opener->getChar(), $characters)) {
111 396
                break;
112
            }
113 72
            $opener = $opener->getPrevious();
114 72
        }
115
116 405
        return $opener;
117
    }
118
119
    /**
120
     * @param string|string[] $characters
121
     * @param callable        $callback
122
     * @param Delimiter       $stackBottom
123
     */
124 1656
    public function iterateByCharacters($characters, $callback, Delimiter $stackBottom = null)
125
    {
126 1656
        if (!is_array($characters)) {
127
            $characters = [$characters];
128
        }
129
130 1656
        $openersBottom = array_fill_keys($characters, $stackBottom);
131
132
        // Find first closer above stackBottom
133 1656
        $closer = $this->findEarliest($stackBottom);
134
135 1656
        while ($closer !== null) {
136 807
            $closerChar = $closer->getChar();
137
138 807
            if (!$closer->canClose() || !in_array($closerChar, $characters)) {
139 768
                $closer = $closer->getNext();
140 768
                continue;
141
            }
142
143 408
            $oddMatch = false;
144 408
            $opener = $this->findMatchingOpener($closer, $openersBottom, $stackBottom, $oddMatch);
145 408
            if ($opener) {
146 336
                $closer = $callback($opener, $closer, $this);
147 408
            } elseif ($oddMatch) {
148 15
                $closer = $closer->getNext();
149 15
            } else {
150 117
                $oldCloser = $closer;
151 117
                $closer = $closer->getNext();
152
                // Set lower bound for future searches for openers:
153 117
                $openersBottom[$closerChar] = $oldCloser->getPrevious();
154 117
                if (!$oldCloser->canOpen()) {
155
                    // We can remove a closer that can't be an opener,
156
                    // once we've seen there's no matching opener:
157 87
                    $this->removeDelimiter($oldCloser);
158 87
                }
159 117
                continue;
160
            }
161 336
        }
162 1656
    }
163
164
    /**
165
     * @param Delimiter      $closer
166
     * @param array          $openersBottom
167
     * @param Delimiter|null $stackBottom
168
     * @param bool           $oddMatch
169
     *
170
     * @return Delimiter|null
171
     */
172 408
    protected function findMatchingOpener(Delimiter $closer, $openersBottom, Delimiter $stackBottom = null, &$oddMatch = false)
173
    {
174 408
        $closerChar = $closer->getChar();
175 408
        $opener = $closer->getPrevious();
176
177 408
        while ($opener !== null && $opener !== $stackBottom && $opener !== $openersBottom[$closerChar]) {
178 384
            $oddMatch = ($closer->canOpen() || $opener->canClose()) && ($opener->getOrigDelims() + $closer->getOrigDelims()) % 3 === 0;
179 384
            if ($opener->getChar() === $closerChar && $opener->canOpen() && !$oddMatch) {
180 336
                return $opener;
181
            }
182
183 81
            $opener = $opener->getPrevious();
184 81
        }
185 132
    }
186
187
    /**
188
     * @param Delimiter      $closer
189
     * @param array          $openersBottom
190
     * @param Delimiter|null $stackBottom
191
     *
192
     * @return Delimiter|null
193
     *
194
     * @deprecated Use findMatchingOpener() instead.  This method will be removed in the next major release.
195
     */
196
    protected function findFirstMatchingOpener(Delimiter $closer, $openersBottom, Delimiter $stackBottom = null)
197
    {
198
        $closerChar = $closer->getChar();
199
        $opener = $closer->getPrevious();
200
201
        while ($opener !== null && $opener !== $stackBottom && $opener !== $openersBottom[$closerChar]) {
202
            if ($opener->getChar() === $closerChar && $opener->canOpen()) {
203
                return $opener;
204
            }
205
206
            $opener = $opener->getPrevious();
207
        }
208
    }
209
}
210