Completed
Push — optimizations ( 75e26b )
by Colin
03:14
created

src/DocParser.php (1 issue)

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
 * 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;
16
17
use League\CommonMark\Block\Element\AbstractBlock;
18
use League\CommonMark\Block\Element\Document;
19
use League\CommonMark\Block\Element\InlineContainerInterface;
20
use League\CommonMark\Block\Element\Paragraph;
21
use League\CommonMark\Node\NodeWalker;
22
23
class DocParser
24
{
25
    /**
26
     * @var Environment
27
     */
28
    protected $environment;
29
30
    /**
31
     * @var InlineParserEngine
32
     */
33
    private $inlineParserEngine;
34
35
    /**
36
     * @var int|float
37
     */
38
    private $maxNestingLevel;
39
40
    /**
41
     * @param Environment $environment
42
     */
43 1950
    public function __construct(Environment $environment)
44 2
    {
45 1950
        $this->environment = $environment;
46 1950
        $this->inlineParserEngine = new InlineParserEngine($environment);
47 1950
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', INF);
48 1950
    }
49
50
    /**
51
     * @return Environment
52
     */
53 1950
    public function getEnvironment()
54
    {
55 1950
        return $this->environment;
56
    }
57
58
    /**
59
     * @param string $input
60
     *
61
     * @return string[]
62
     */
63 1941
    private function preProcessInput($input)
64 1
    {
65 1941
        $lines = preg_split('/\r\n|\n|\r/', $input);
66
67
        // Remove any newline which appears at the very end of the string.
68
        // We've already split the document by newlines, so we can simply drop
69
        // any empty element which appears on the end.
70 1941
        if (end($lines) === '') {
71 1926
            array_pop($lines);
72 642
        }
73
74 1941
        return $lines;
75
    }
76
77
    /**
78
     * @param string $input
79
     *
80
     * @return Document
81
     */
82 1941
    public function parse($input)
83
    {
84 1941
        $context = new Context(new Document(), $this->getEnvironment());
85 1941
        $context->setEncoding(mb_detect_encoding($input, 'ASCII,UTF-8', true) ?: 'ISO-8859-1');
86
87 1941
        $lines = $this->preProcessInput($input);
88 1941
        foreach ($lines as $line) {
89 1938
            $context->setNextLine($line);
90 1938
            $this->incorporateLine($context);
91 647
        }
92
93 1941
        $lineCount = count($lines);
94 1941
        while ($tip = $context->getTip()) {
95 1941
            $tip->finalize($context, $lineCount);
96 647
        }
97
98 1941
        $this->processInlines($context, $context->getDocument()->walker());
99
100 1941
        $this->processDocument($context);
101
102 1941
        return $context->getDocument();
103
    }
104
105 1938
    private function incorporateLine(ContextInterface $context)
106
    {
107 1938
        $context->getBlockCloser()->resetTip();
108 1938
        $context->setBlocksParsed(false);
109
110 1938
        $cursor = new Cursor($context->getLine(), $context->getEncoding());
111
112 1938
        $this->resetContainer($context, $cursor);
113 1938
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
114
115 1938
        $this->parseBlocks($context, $cursor);
116
117
        // What remains at the offset is a text line.  Add the text to the appropriate container.
118
        // First check for a lazy paragraph continuation:
119 1938
        if ($this->isLazyParagraphContinuation($context, $cursor)) {
120
            // lazy paragraph continuation
121 33
            $context->getTip()->addLine($cursor->getRemainder());
122
123 33
            return;
124
        }
125
126
        // not a lazy continuation
127
        // finalize any blocks not matched
128 1938
        $context->getBlockCloser()->closeUnmatchedBlocks();
129
130
        // Determine whether the last line is blank, updating parents as needed
131 1938
        $this->setAndPropagateLastLineBlank($context, $cursor);
132
133
        // Handle any remaining cursor contents
134 1938
        if ($context->getContainer()->acceptsLines()) {
135 726
            $context->getContainer()->handleRemainingContents($context, $cursor);
136 1774
        } elseif (!$cursor->isBlank()) {
137
            // Create paragraph container for line
138 1614
            $context->addBlock(new Paragraph());
139 1614
            $cursor->advanceToNextNonSpaceOrTab();
140 1614
            $context->getTip()->addLine($cursor->getRemainder());
141 538
        }
142 1938
    }
143
144 1941
    private function processDocument(ContextInterface $context)
145
    {
146 1941
        foreach ($this->getEnvironment()->getDocumentProcessors() as $documentProcessor) {
147
            $documentProcessor->processDocument($context->getDocument());
148 647
        }
149 1941
    }
150
151 1941
    private function processInlines(ContextInterface $context, NodeWalker $walker)
152
    {
153 1941
        while ($event = $walker->next()) {
154 1941
            if (!$event->isEntering()) {
155 1941
                continue;
156
            }
157
158 1941
            $node = $event->getNode();
159 1941
            if ($node instanceof InlineContainerInterface) {
160 1662
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
161 554
            }
162 647
        }
163 1941
    }
164
165
    /**
166
     * Sets the container to the last open child (or its parent)
167
     *
168
     * @param ContextInterface $context
169
     * @param Cursor           $cursor
170
     */
171 1938
    private function resetContainer(ContextInterface $context, Cursor $cursor)
172
    {
173 1938
        $container = $context->getDocument();
174
175 1938
        while ($lastChild = $container->lastChild()) {
176 1080
            if (!$lastChild->isOpen()) {
177 450
                break;
178
            }
179
180 1071
            $container = $lastChild;
181 1071
            if (!$container->matchesNextLine($cursor)) {
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class League\CommonMark\Node\Node as the method matchesNextLine() does only exist in the following sub-classes of League\CommonMark\Node\Node: League\CommonMark\Block\Element\AbstractBlock, League\CommonMark\Block\Element\BlockQuote, League\CommonMark\Block\Element\Document, League\CommonMark\Block\Element\FencedCode, League\CommonMark\Block\Element\Heading, League\CommonMark\Block\Element\HtmlBlock, League\CommonMark\Block\Element\IndentedCode, League\CommonMark\Block\Element\ListBlock, League\CommonMark\Block\Element\ListItem, League\CommonMark\Block\Element\Paragraph, League\CommonMark\Block\Element\ThematicBreak. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
182 693
                $container = $container->parent(); // back up to the last matching block
183 693
                break;
184
            }
185 250
        }
186
187 1938
        $context->setContainer($container);
188 1938
    }
189
190
    /**
191
     * Parse blocks
192
     *
193
     * @param ContextInterface $context
194
     * @param Cursor           $cursor
195
     */
196 1938
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
197
    {
198 1938
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
199 1938
            $parsed = false;
200 1938
            foreach ($this->environment->getBlockParsers() as $parser) {
201 1938
                if ($parser->parse($context, $cursor)) {
202 804
                    $parsed = true;
203 1560
                    break;
204
                }
205 646
            }
206
207 1938
            if (!$parsed || $context->getContainer()->acceptsLines() || $context->getTip()->getDepth() >= $this->maxNestingLevel) {
208 1911
                $context->setBlocksParsed(true);
209 1911
                break;
210
            }
211 132
        }
212 1938
    }
213
214
    /**
215
     * @param ContextInterface $context
216
     * @param Cursor           $cursor
217
     *
218
     * @return bool
219
     */
220 1938
    private function isLazyParagraphContinuation(ContextInterface $context, Cursor $cursor)
221
    {
222 1938
        return $context->getTip() instanceof Paragraph &&
223 1938
            !$context->getBlockCloser()->areAllClosed() &&
224 1938
            !$cursor->isBlank() &&
225 1938
            count($context->getTip()->getStrings()) > 0;
226
    }
227
228
    /**
229
     * @param ContextInterface $context
230
     * @param Cursor           $cursor
231
     */
232 1938
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
233
    {
234 1938
        $container = $context->getContainer();
235
236 1938
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
237 462
            if ($lastChild instanceof AbstractBlock) {
238 462
                $lastChild->setLastLineBlank(true);
239 154
            }
240 154
        }
241
242 1938
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
243
244
        // Propagate lastLineBlank up through parents:
245 1938
        while ($container) {
246 1938
            $container->setLastLineBlank($lastLineBlank);
247 1938
            $container = $container->parent();
248 646
        }
249 1938
    }
250
}
251