Completed
Push — develop ( d73a05...7ce957 )
by Mike
07:24
created

Compiler/Pass/ResolveInlineLinkAndSeeTags.php (1 issue)

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
declare(strict_types=1);
3
4
/**
5
 * This file is part of phpDocumentor.
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author    Mike van Riel <[email protected]>
11
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      http://phpdoc.org
14
 */
15
16
namespace phpDocumentor\Compiler\Pass;
17
18
use phpDocumentor\Compiler\CompilerPassInterface;
19
use phpDocumentor\Descriptor\Collection;
20
use phpDocumentor\Descriptor\DescriptorAbstract;
21
use phpDocumentor\Descriptor\ProjectDescriptor;
22
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
23
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
24
use phpDocumentor\Reflection\DocBlock\Tag;
25
use phpDocumentor\Reflection\DocBlock\Tags\Link;
26
use phpDocumentor\Reflection\DocBlock\Tags\Reference\Fqsen;
27
use phpDocumentor\Reflection\DocBlock\Tags\See;
28
use phpDocumentor\Reflection\FqsenResolver;
29
use phpDocumentor\Reflection\TypeResolver;
30
use phpDocumentor\Reflection\Types\Context;
31
use phpDocumentor\Transformer\Router\Queue;
32
33
/**
34
 * This step in the compilation process iterates through all elements and scans their descriptions for an inline `@see`
35
 * or `@link` tag and resolves them to a markdown link.
36
 */
37
class ResolveInlineLinkAndSeeTags implements CompilerPassInterface
38
{
39
    const COMPILER_PRIORITY = 9002;
40
41
    const REGEX_INLINE_LINK_OR_SEE_TAG = '/\{\@(see|link)[\ ]+([^\}]+)\}/';
42
43
    /** @var Queue */
44
    private $router;
45
46
    /** @var DescriptorAbstract */
47
    private $descriptor;
48
49
    /** @var Collection */
50
    private $elementCollection;
51
52
    /**
53
     * Registers the router queue with this pass.
54
     */
55 5
    public function __construct(Queue $router)
56
    {
57 5
        $this->router = $router;
58 5
    }
59
60 1
    public function getDescription(): string
61
    {
62 1
        return 'Resolve @link and @see tags in descriptions';
63
    }
64
65
    /**
66
     * Iterates through each element in the project and replaces its inline @see and @link tag with a markdown
67
     * representation.
68
     */
69 4
    public function execute(ProjectDescriptor $project): void
70
    {
71
        /** @var Collection|DescriptorAbstract[] $elementCollection */
72 4
        $this->elementCollection = $project->getIndexes()->get('elements');
73
74 4
        foreach ($this->elementCollection as $descriptor) {
75 4
            $this->resolveSeeAndLinkTags($descriptor);
76
        }
77 4
    }
78
79
    /**
80
     * Resolves all @see and @link tags in the description of the given descriptor to their markdown representation.
81
     *
82
     * @uses self::resolveTag()
83
     */
84 4
    private function resolveSeeAndLinkTags(DescriptorAbstract $descriptor): void
85
    {
86
        // store descriptor to use it in the resolveTag method
87 4
        $this->descriptor = $descriptor;
88
89 4
        $descriptor->setDescription(
90 4
            preg_replace_callback(
91 4
                self::REGEX_INLINE_LINK_OR_SEE_TAG,
92 4
                [$this, 'resolveTag'],
93 4
                $descriptor->getDescription()
94
            )
95
        );
96 4
    }
97
98
    /**
99
     * Resolves an individual tag, indicated by the results of the Regex used to extract tags.
100
     *
101
     * @param string[] $match
102
     * @return string|string[]
103
     */
104 2
    private function resolveTag(array $match)
105
    {
106 2
        $tagReflector = $this->createLinkOrSeeTagFromRegexMatch($match);
107 2
        if (!$tagReflector instanceof See && !$tagReflector instanceof Link) {
108
            return $match;
109
        }
110
111 2
        $link = $this->getLinkText($tagReflector);
112 2
        $description = (string) $tagReflector->getDescription();
113
114 2
        if ($this->isUrl($link)) {
115
            return $this->generateMarkdownLink($link, $description ?: $link);
116
        }
117
118 2
        $link = $this->resolveQsen($link);
119 2
        $element = $this->findElement($link);
120 2
        if (!$element) {
121 1
            return (string) $link;
122
        }
123
124 1
        return $this->resolveElement($element, $link, $description);
125
    }
126
127
    /**
128
     * Determines if the given link string represents a URL by checking if it is prefixed with a URI scheme.
129
     */
130 2
    private function isUrl(string $link): bool
131
    {
132 2
        return (bool) preg_match('/^[\w]+:\/\/.+$/', $link);
133
    }
134
135
    /**
136
     * Checks if the link represents a Fully Qualified Structural Element Name.
137
     *
138
     * @param Fqsen|string $link
139
     */
140 2
    private function isFqsen($link): bool
141
    {
142 2
        return $link instanceof Fqsen;
143
    }
144
145
    /**
146
     * Creates a Tag Reflector from the given array of tag line, tag name and tag content.
147
     *
148
     * @param string[] $match
149
     */
150 2
    private function createLinkOrSeeTagFromRegexMatch(array $match): Tag
151
    {
152 2
        list($completeMatch, $tagName, $tagContent) = $match;
0 ignored issues
show
The assignment to $completeMatch is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
153
154 2
        $fqsenResolver = new FqsenResolver();
155 2
        $tagFactory = new StandardTagFactory($fqsenResolver);
156 2
        $descriptionFactory = new DescriptionFactory($tagFactory);
157 2
        $tagFactory->addService($descriptionFactory);
158 2
        $tagFactory->addService(new TypeResolver($fqsenResolver));
159
160
        switch ($tagName) {
161 2
            case 'see':
162 2
                return See::create($tagContent, $fqsenResolver, $descriptionFactory, $this->createDocBlockContext());
163
            case 'link':
164
                return Link::create($tagContent, $descriptionFactory, $this->createDocBlockContext());
165
        }
166
    }
167
168
    /**
169
     * Resolves a QSEN to a FQSEN.
170
     *
171
     * If a relative QSEN is provided then this method will attempt to resolve it given the current namespace and
172
     * namespace aliases.
173
     *
174
     * @param Fqsen|string $link
175
     * @return Fqsen|string
176
     */
177 2
    private function resolveQsen($link)
178
    {
179 2
        if (!$this->isFqsen($link)) {
180 2
            return $link;
181
        }
182
183
        return $link;
184
    }
185
186
    /**
187
     * Generates a Markdown link to the given Descriptor or returns the link text if no route to the Descriptor could
188
     * be matched.
189
     *
190
     * @param Fqsen|string $link
191
     */
192 1
    private function resolveElement(DescriptorAbstract $element, $link, ?string $description = null): string
193
    {
194 1
        $rule = $this->router->match($element);
195
196 1
        if ($rule) {
197 1
            $url = '..' . $rule->generate($element);
198 1
            $link = $this->generateMarkdownLink($url, $description ?: (string) $link);
199
        }
200
201 1
        return $link;
202
    }
203
204
    /**
205
     * Returns the link for the given reflector.
206
     *
207
     * Because the link tag and the see tag have different methods to acquire the link text we abstract that into this
208
     * method.
209
     */
210 2
    private function getLinkText(Tag $tagReflector): ?string
211
    {
212 2
        if ($tagReflector instanceof See) {
213 2
            return (string) $tagReflector->getReference();
214
        }
215
216
        if ($tagReflector instanceof Link) {
217
            return (string) $tagReflector->getLink();
218
        }
219
220
        return null;
221
    }
222
223
    /**
224
     * Tries to find an element with the given FQSEN in the elements listing for this project.
225
     *
226
     * @param Fqsen|string $fqsen
227
     */
228 2
    private function findElement($fqsen): ?DescriptorAbstract
229
    {
230 2
        return $this->elementCollection[(string) $fqsen] ?? null;
231
    }
232
233
    /**
234
     * Creates a DocBlock context containing the namespace and aliases for the current descriptor.
235
     */
236 2
    private function createDocBlockContext(): Context
237
    {
238 2
        $file = $this->descriptor->getFile();
239 2
        $namespaceAliases = $file ? $file->getNamespaceAliases()->getAll() : [];
240 2
        foreach ($namespaceAliases as $alias => $fqsen) {
241 2
            $namespaceAliases[$alias] = (string) $fqsen;
242
        }
243
244 2
        return new Context((string) $this->descriptor->getNamespace(), $namespaceAliases);
245
    }
246
247
    /**
248
     * Generates a Markdown-formatted string representing a link with a description.
249
     *
250
     * @param Fqsen|string $link
251
     */
252 1
    private function generateMarkdownLink($link, string $description): string
253
    {
254 1
        return '[' . $description . '](' . (string) $link . ')';
255
    }
256
}
257