Completed
Push — master ( aeeeb2...74d969 )
by Colin
03:28
created

src/DocParser.php (2 issues)

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\Block\Element\StringContainerInterface;
22
23
class DocParser
24
{
25
    /**
26
     * @var EnvironmentInterface
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 EnvironmentInterface $environment
42
     */
43 2037
    public function __construct(EnvironmentInterface $environment)
44
    {
45 2037
        $this->environment = $environment;
46 2037
        $this->inlineParserEngine = new InlineParserEngine($environment);
47 2037
        $this->maxNestingLevel = $environment->getConfig('max_nesting_level', INF);
48 2037
    }
49
50
    /**
51
     * @return EnvironmentInterface
52
     */
53 2037
    public function getEnvironment(): EnvironmentInterface
54
    {
55 2037
        return $this->environment;
56
    }
57
58
    /**
59
     * @param string $input
60
     *
61
     * @return string[]
62
     */
63 2028
    private function preProcessInput(string $input): array
64
    {
65 2028
        $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 2028
        if (\end($lines) === '') {
71 2013
            \array_pop($lines);
72
        }
73
74 2028
        return $lines;
75
    }
76
77
    /**
78
     * @param string $input
79
     *
80
     * @return Document
81
     */
82 2028
    public function parse(string $input): Document
83
    {
84 2028
        $context = new Context(new Document(), $this->getEnvironment());
85 2028
        $context->setEncoding(\mb_detect_encoding($input, 'ASCII,UTF-8', true) ?: 'ISO-8859-1');
86
87 2028
        $lines = $this->preProcessInput($input);
88 2028
        foreach ($lines as $line) {
89 2028
            $context->setNextLine($line);
90 2028
            $this->incorporateLine($context);
91
        }
92
93 2028
        $lineCount = \count($lines);
94 2028
        while ($tip = $context->getTip()) {
95 2028
            $tip->finalize($context, $lineCount);
96
        }
97
98 2028
        $this->processInlines($context);
99
100 2028
        $this->processDocument($context);
101
102 2028
        return $context->getDocument();
103
    }
104
105 2028
    private function incorporateLine(ContextInterface $context)
106
    {
107 2028
        $context->getBlockCloser()->resetTip();
108 2028
        $context->setBlocksParsed(false);
109
110 2028
        $cursor = new Cursor($context->getLine(), $context->getEncoding());
111
112 2028
        $this->resetContainer($context, $cursor);
113 2028
        $context->getBlockCloser()->setLastMatchedContainer($context->getContainer());
114
115 2028
        $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 2028
        if ($this->isLazyParagraphContinuation($context, $cursor)) {
120
            // lazy paragraph continuation
121 36
            $context->getTip()->addLine($cursor->getRemainder());
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class League\CommonMark\Block\Element\AbstractBlock as the method addLine() does only exist in the following sub-classes of League\CommonMark\Block\Element\AbstractBlock: League\CommonMark\Block\...actStringContainerBlock, League\CommonMark\Block\Element\FencedCode, League\CommonMark\Block\Element\Heading, League\CommonMark\Block\Element\HtmlBlock, League\CommonMark\Block\Element\IndentedCode, League\CommonMark\Block\Element\Paragraph. 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...
122
123 36
            return;
124
        }
125
126
        // not a lazy continuation
127
        // finalize any blocks not matched
128 2028
        $context->getBlockCloser()->closeUnmatchedBlocks();
129
130
        // Determine whether the last line is blank, updating parents as needed
131 2028
        $this->setAndPropagateLastLineBlank($context, $cursor);
132
133
        // Handle any remaining cursor contents
134 2028
        if ($context->getContainer() instanceof StringContainerInterface) {
135 759
            $context->getContainer()->handleRemainingContents($context, $cursor);
136 1779
        } elseif (!$cursor->isBlank()) {
137
            // Create paragraph container for line
138 1698
            $context->addBlock(new Paragraph());
139 1698
            $cursor->advanceToNextNonSpaceOrTab();
140 1698
            $context->getTip()->addLine($cursor->getRemainder());
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class League\CommonMark\Block\Element\AbstractBlock as the method addLine() does only exist in the following sub-classes of League\CommonMark\Block\Element\AbstractBlock: League\CommonMark\Block\...actStringContainerBlock, League\CommonMark\Block\Element\FencedCode, League\CommonMark\Block\Element\Heading, League\CommonMark\Block\Element\HtmlBlock, League\CommonMark\Block\Element\IndentedCode, League\CommonMark\Block\Element\Paragraph. 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...
141
        }
142 2028
    }
143
144 2028
    private function processDocument(ContextInterface $context)
145
    {
146 2028
        foreach ($this->getEnvironment()->getDocumentProcessors() as $documentProcessor) {
147
            $documentProcessor->processDocument($context->getDocument());
148
        }
149 2028
    }
150
151 2028
    private function processInlines(ContextInterface $context)
152
    {
153 2028
        $walker = $context->getDocument()->walker();
154
155 2028
        while ($event = $walker->next()) {
156 2028
            if (!$event->isEntering()) {
157 2028
                continue;
158
            }
159
160 2028
            $node = $event->getNode();
161 2028
            if ($node instanceof InlineContainerInterface) {
162 1743
                $this->inlineParserEngine->parse($node, $context->getDocument()->getReferenceMap());
163
            }
164
        }
165 2028
    }
166
167
    /**
168
     * Sets the container to the last open child (or its parent)
169
     *
170
     * @param ContextInterface $context
171
     * @param Cursor           $cursor
172
     */
173 2028
    private function resetContainer(ContextInterface $context, Cursor $cursor)
174
    {
175 2028
        $container = $context->getDocument();
176
177 2028
        while ($lastChild = $container->lastChild()) {
178 1125
            if (!($lastChild instanceof AbstractBlock)) {
179
                break;
180
            }
181
182 1125
            if (!$lastChild->isOpen()) {
183 459
                break;
184
            }
185
186 1116
            $container = $lastChild;
187 1116
            if (!$container->matchesNextLine($cursor)) {
188 714
                $container = $container->parent(); // back up to the last matching block
189 714
                break;
190
            }
191
        }
192
193 2028
        $context->setContainer($container);
194 2028
    }
195
196
    /**
197
     * Parse blocks
198
     *
199
     * @param ContextInterface $context
200
     * @param Cursor           $cursor
201
     */
202 2028
    private function parseBlocks(ContextInterface $context, Cursor $cursor)
203
    {
204 2028
        while (!$context->getContainer()->isCode() && !$context->getBlocksParsed()) {
205 2028
            $parsed = false;
206 2028
            foreach ($this->environment->getBlockParsers() as $parser) {
207 2028
                if ($parser->parse($context, $cursor)) {
208 825
                    $parsed = true;
209 1226
                    break;
210
                }
211
            }
212
213 2028
            if (!$parsed || $context->getContainer() instanceof StringContainerInterface || $context->getTip()->getDepth() >= $this->maxNestingLevel) {
214 2001
                $context->setBlocksParsed(true);
215 2001
                break;
216
            }
217
        }
218 2028
    }
219
220
    /**
221
     * @param ContextInterface $context
222
     * @param Cursor           $cursor
223
     *
224
     * @return bool
225
     */
226 2028
    private function isLazyParagraphContinuation(ContextInterface $context, Cursor $cursor): bool
227
    {
228 2028
        return $context->getTip() instanceof Paragraph &&
229 2028
            !$context->getBlockCloser()->areAllClosed() &&
230 2028
            !$cursor->isBlank() &&
231 2028
            \count($context->getTip()->getStrings()) > 0;
232
    }
233
234
    /**
235
     * @param ContextInterface $context
236
     * @param Cursor           $cursor
237
     */
238 2028
    private function setAndPropagateLastLineBlank(ContextInterface $context, Cursor $cursor)
239
    {
240 2028
        $container = $context->getContainer();
241
242 2028
        if ($cursor->isBlank() && $lastChild = $container->lastChild()) {
243 474
            if ($lastChild instanceof AbstractBlock) {
244 474
                $lastChild->setLastLineBlank(true);
245
            }
246
        }
247
248 2028
        $lastLineBlank = $container->shouldLastLineBeBlank($cursor, $context->getLineNumber());
249
250
        // Propagate lastLineBlank up through parents:
251 2028
        while ($container instanceof AbstractBlock) {
252 2028
            $container->setLastLineBlank($lastLineBlank);
253 2028
            $container = $container->parent();
254
        }
255 2028
    }
256
}
257