Completed
Push — develop ( 9193e7...62056c )
by Jaap
12:45 queued 02:43
created

Compiler/Pass/ResolveInlineLinkAndSeeTags.php (4 issues)

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
 * phpDocumentor
4
 *
5
 * PHP Version 5.3
6
 *
7
 * @copyright 2010-2014 Mike van Riel / Naenius (http://www.naenius.com)
8
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
9
 * @link      http://phpdoc.org
10
 */
11
12
namespace phpDocumentor\Compiler\Pass;
13
14
use phpDocumentor\Descriptor\Collection;
15
use phpDocumentor\Descriptor\DescriptorAbstract;
16
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
17
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
18
use phpDocumentor\Reflection\DocBlock\Tag;
19
use phpDocumentor\Reflection\DocBlock\Tags\Link;
20
use phpDocumentor\Reflection\DocBlock\Tags\Reference\Fqsen;
21
use phpDocumentor\Reflection\DocBlock\Tags\See;
22
use phpDocumentor\Reflection\FqsenResolver;
23
use phpDocumentor\Reflection\TypeResolver;
24
use phpDocumentor\Reflection\Types\Context;
25
use phpDocumentor\Transformer\Router\Queue;
26
use phpDocumentor\Transformer\Router\RouterAbstract;
27
use phpDocumentor\Compiler\CompilerPassInterface;
28
use phpDocumentor\Descriptor\ProjectDescriptor;
29
30
/**
31
 * This step in the compilation process iterates through all elements and scans their descriptions for an inline `@see`
32
 * or `@link` tag and resolves them to a markdown link.
33
 *
34
 */
35
class ResolveInlineLinkAndSeeTags implements CompilerPassInterface
0 ignored issues
show
The class ResolveInlineLinkAndSeeTags has a coupling between objects value of 15. Consider to reduce the number of dependencies under 13.
Loading history...
36
{
37
    const COMPILER_PRIORITY = 9002;
38
    const REGEX_INLINE_LINK_OR_SEE_TAG = '/\{\@(see|link)[\ ]+([^\}]+)\}/';
39
40
    /** @var RouterAbstract */
41
    private $router;
42
43
    /** @var DescriptorAbstract */
44
    private $descriptor;
45
46
    /** @var Collection */
47
    private $elementCollection;
48
49
    /**
50
     * Registers the router queue with this pass.
51
     */
52
    public function __construct(Queue $router)
53
    {
54
        $this->router = $router;
55
    }
56
57
    /**
58
     * {@inheritDoc}
59
     */
60
    public function getDescription()
61
    {
62
        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
     * @param ProjectDescriptor $project
70
     *
71
     * @return void
72
     */
73
    public function execute(ProjectDescriptor $project)
74
    {
75
        /** @var Collection|DescriptorAbstract[] $elementCollection */
76
        $this->elementCollection = $project->getIndexes()->get('elements');
77
78
        foreach ($this->elementCollection as $descriptor) {
79
            $this->resolveSeeAndLinkTags($descriptor);
80
        }
81
    }
82
83
    /**
84
     * Resolves all @see and @link tags in the description of the given descriptor to their markdown representation.
85
     *
86
     * @param DescriptorAbstract $descriptor
87
     *
88
     * @uses self::resolveTag()
89
     *
90
     * @return void
91
     */
92
    private function resolveSeeAndLinkTags(DescriptorAbstract $descriptor)
93
    {
94
        // store descriptor to use it in the resolveTag method
95
        $this->descriptor = $descriptor;
96
97
        $descriptor->setDescription(
98
            preg_replace_callback(
99
                self::REGEX_INLINE_LINK_OR_SEE_TAG,
100
                array($this, 'resolveTag'),
101
                $descriptor->getDescription()
102
            )
103
        );
104
    }
105
106
    /**
107
     * Resolves an individual tag, indicated by the results of the Regex used to extract tags.
108
     *
109
     * @param string[] $match
110
     *
111
     * @return string
112
     */
113
    private function resolveTag($match)
114
    {
115
        $tagReflector = $this->createLinkOrSeeTagFromRegexMatch($match);
116
        if (!$tagReflector instanceof See && !$tagReflector instanceof Link) {
117
            return $match;
118
        }
119
120
        $link        = $this->getLinkText($tagReflector);
121
        $description = $tagReflector->getDescription();
122
123
        if ($this->isUrl($link)) {
0 ignored issues
show
It seems like $link defined by $this->getLinkText($tagReflector) on line 120 can also be of type null or object<phpDocumentor\Ref...gs\Reference\Reference>; however, phpDocumentor\Compiler\P...LinkAndSeeTags::isUrl() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
124
            return $this->generateMarkdownLink($link, $description ?: $link);
0 ignored issues
show
It seems like $link defined by $this->getLinkText($tagReflector) on line 120 can also be of type null or object<phpDocumentor\Ref...gs\Reference\Reference>; however, phpDocumentor\Compiler\P...:generateMarkdownLink() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
125
        }
126
127
        $link    = $this->resolveQsen($link);
0 ignored issues
show
It seems like $link can also be of type null or object<phpDocumentor\Ref...gs\Reference\Reference>; however, phpDocumentor\Compiler\P...dSeeTags::resolveQsen() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
128
        $element = $this->findElement($link);
129
        if (!$element) {
130
            return $link;
131
        }
132
133
        return $this->resolveElement($element, $link, $description);
134
    }
135
136
    /**
137
     * Determines if the given link string represents a URL by checking if it is prefixed with a URI scheme.
138
     *
139
     * @param string $link
140
     *
141
     * @return boolean
142
     */
143
    private function isUrl($link)
144
    {
145
        return (bool) preg_match('/^[\w]+:\/\/.+$/', $link);
146
    }
147
148
    /**
149
     * Checks if the link represents a Fully Qualified Structural Element Name.
150
     *
151
     * @param string $link
152
     *
153
     * @return bool
154
     */
155
    private function isFqsen($link)
156
    {
157
        return $link instanceof Fqsen;
158
    }
159
160
    /**
161
     * Creates a Tag Reflector from the given array of tag line, tag name and tag content.
162
     *
163
     * @param string[] $match
164
     *
165
     * @return Tag
166
     */
167
    private function createLinkOrSeeTagFromRegexMatch(array $match)
168
    {
169
        list($completeMatch, $tagName, $tagContent) = $match;
170
171
        $fqsenResolver = new FqsenResolver();
172
        $tagFactory = new StandardTagFactory($fqsenResolver);
173
        $descriptionFactory = new DescriptionFactory($tagFactory);
174
        $tagFactory->addService($descriptionFactory);
175
        $tagFactory->addService(new TypeResolver($fqsenResolver));
176
177
        switch ($tagName) {
178
            case 'see':
179
                return See::create($tagContent, $fqsenResolver, $descriptionFactory, $this->createDocBlockContext());
180
            case 'link':
181
                return Link::create($tagContent, $descriptionFactory, $this->createDocBlockContext());
182
        }
183
    }
184
185
    /**
186
     * Resolves a QSEN to a FQSEN.
187
     *
188
     * If a relative QSEN is provided then this method will attempt to resolve it given the current namespace and
189
     * namespace aliases.
190
     *
191
     * @param string $link
192
     *
193
     * @return string
194
     */
195
    private function resolveQsen($link)
196
    {
197
        if (!$this->isFqsen($link)) {
198
            return $link;
199
        }
200
201
        return $link;
202
    }
203
204
    /**
205
     * Generates a Markdown link to the given Descriptor or returns the link text if no route to the Descriptor could
206
     * be matched.
207
     *
208
     * @param DescriptorAbstract $element
209
     * @param string             $link
210
     * @param string             $description
211
     *
212
     * @return string
213
     */
214
    private function resolveElement(DescriptorAbstract $element, $link, $description)
215
    {
216
        $rule = $this->router->match($element);
217
218
        if ($rule) {
219
            $url = '..' . $rule->generate($element);
220
            $link = $this->generateMarkdownLink($url, $description ? : $link);
221
        }
222
223
        return $link;
224
    }
225
226
    /**
227
     * Returns the link for the given reflector.
228
     *
229
     * Because the link tag and the see tag have different methods to acquire the link text we abstract that into this
230
     * method.
231
     *
232
     * @param See|Link $tagReflector
233
     *
234
     * @return string
235
     */
236
    private function getLinkText($tagReflector)
237
    {
238
        if ($tagReflector instanceof See) {
239
            return $tagReflector->getReference();
240
        }
241
242
        if ($tagReflector instanceof Link) {
243
            return $tagReflector->getLink();
244
        }
245
246
        return null;
247
    }
248
249
    /**
250
     * Tries to find an element with the given FQSEN in the elements listing for this project.
251
     *
252
     * @param string $fqsen
253
     *
254
     * @return DescriptorAbstract|null
255
     */
256
    private function findElement($fqsen)
257
    {
258
        return isset($this->elementCollection[(string)$fqsen]) ? $this->elementCollection[(string)$fqsen] : null;
259
    }
260
261
    /**
262
     * Creates a DocBlock context containing the namespace and aliases for the current descriptor.
263
     *
264
     * @return Context
265
     */
266
    private function createDocBlockContext()
267
    {
268
        $file = $this->descriptor->getFile();
269
        $namespaceAliases = $file ? $file->getNamespaceAliases()->getAll() : array();
270
        foreach ($namespaceAliases as $alias => $fqsen) {
271
            $namespaceAliases[$alias] = (string)$fqsen;
272
        }
273
274
        return new Context((string)$this->descriptor->getNamespace(), $namespaceAliases);
275
    }
276
277
    /**
278
     * Generates a Markdown-formatted string representing a link with a description.
279
     *
280
     * @param string $link
281
     * @param string $description
282
     *
283
     * @return string
284
     */
285
    private function generateMarkdownLink($link, $description)
286
    {
287
        return '[' . $description . '](' . $link . ')';
288
    }
289
}
290