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

src/DocParser.php (2 issues)

strict.coding_against_specific_subtype

Bug Minor

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