AnnotationParser::addAnnotations()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\Parser\AnnotationParser
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Bernhard Wick <[email protected]>
15
 * @copyright 2015 TechDivision GmbH - <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/doppelgaenger
18
 * @link      http://www.appserver.io/
19
 */
20
21
namespace AppserverIo\Doppelgaenger\Parser;
22
23
use AppserverIo\Doppelgaenger\Entities\Assertions\AssertionFactory;
24
use AppserverIo\Doppelgaenger\Entities\Assertions\RawAssertion;
25
use AppserverIo\Doppelgaenger\Entities\Assertions\TypedCollectionAssertion;
26
use AppserverIo\Doppelgaenger\Entities\Definitions\AttributeDefinition;
27
use AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition;
28
use AppserverIo\Doppelgaenger\Entities\Joinpoint;
29
use AppserverIo\Doppelgaenger\Entities\Lists\AssertionList;
30
use AppserverIo\Doppelgaenger\Entities\Assertions\ChainedAssertion;
31
use AppserverIo\Doppelgaenger\Config;
32
use AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList;
33
use AppserverIo\Doppelgaenger\Entities\PointcutExpression;
34
use AppserverIo\Doppelgaenger\Exceptions\ParserException;
35
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface;
36
use AppserverIo\Doppelgaenger\Interfaces\PropertiedStructureInterface;
37
use AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface;
38
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
39
use AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Introduce;
40
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Ensures;
41
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Invariant;
42
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Requires;
43
use Herrera\Annotations\Tokenizer;
44
use Herrera\Annotations\Tokens;
45
use Herrera\Annotations\Convert\ToArray;
46
47
/**
48
 * The AnnotationParser class which is used to get all usable parts from within DocBlock annotation
49
 *
50
 * @author    Bernhard Wick <[email protected]>
51
 * @copyright 2015 TechDivision GmbH - <[email protected]>
52
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
53
 * @link      https://github.com/appserver-io/doppelgaenger
54
 * @link      http://www.appserver.io/
55
 */
56
class AnnotationParser extends AbstractParser
57
{
58
    /**
59
     * The configuration aspect we need here
60
     *
61
     * @var \AppserverIo\Doppelgaenger\Config $config
62
     */
63
    protected $config;
64
65
    /**
66
     * The annotations which the parser will look for
67
     *
68
     * @var string[] $searchedAnnotations
69
     */
70
    protected $searchedAnnotations;
71
72
    /**
73
     * All valid annotation types we consider complex
74
     *
75
     * @var string[] $validComplexAnnotations
76
     */
77
    protected $validComplexAnnotations = array(
78
        Ensures::ANNOTATION,
79
        Invariant::ANNOTATION,
80
        Requires::ANNOTATION,
81
        Introduce::ANNOTATION
82
    );
83
84
    /**
85
     * Default constructor
86
     *
87
     * @param string                                                                  $file              The path of the file we want to parse
88
     * @param \AppserverIo\Doppelgaenger\Config                                       $config            Configuration
89
     * @param array                                                                   $tokens            The array of tokens taken from the file
90
     * @param \AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface|null $currentDefinition The current definition we are working on
91
     */
92
    public function __construct(
93
        $file,
94
        Config $config,
95
        array & $tokens = array(),
96
        StructureDefinitionInterface $currentDefinition = null
97
    ) {
98
        $this->config = $config;
99
100
        parent::__construct($file, $config, null, null, $currentDefinition, $tokens);
101
    }
102
103
    /**
104
     * Will add an annotation which the parser will then look for on its next run
105
     *
106
     * @param string $annotationString The basic annotation to search for
107
     *
108
     * @return null|false
109
     */
110
    public function addAnnotation($annotationString)
111
    {
112
        // we rely on a leading "@" symbol, so sanitize the input
113
        if (!is_string($annotationString)) {
114
            return false;
115
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
116
        } elseif (substr($annotationString, 0, 1) === '@') {
117
            $annotationString = '@' . $annotationString;
118
        }
119
120
        $this->searchedAnnotations[] = $annotationString;
121
    }
122
123
    /**
124
     * Will add an array of annotations which the parser will then look for on its next run
125
     *
126
     * @param string[] $annotationStrings The basic annotation to search for
127
     *
128
     * @return null
129
     */
130
    public function addAnnotations(array $annotationStrings)
131
    {
132
        foreach ($annotationStrings as $annotationString) {
133
            $this->addAnnotation($annotationString);
134
        }
135
    }
136
137
    /**
138
     * Will return an array containing all annotations of a certain type which where found within a given string
139
     * DocBlock syntax is preferred
140
     *
141
     * @param string $string         String to search in
142
     * @param string $annotationType Name of the annotation (without the leading "@") to search for
143
     *
144
     * @return \stdClass[]
145
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ParserException
146
     */
147
    public function getAnnotationsByType($string, $annotationType)
148
    {
149
        $collectedAnnotations = array();
150
151
        // we have to determine what type of annotations we are searching for, complex (doctrine style) or simple
152
        if (isset(array_flip($this->validComplexAnnotations)[$annotationType])) {
153
            // complex annotations are parsed using herrera-io/php-annotations
154
155
            // get our tokenizer and parse the doc Block
156
            $tokenizer = new Tokenizer();
157
            $tokens = new Tokens($tokenizer->parse($string));
158
159
            // convert to array and run it through our advice factory
160
            $toArray = new ToArray();
161
            $annotations = $toArray->convert($tokens);
162
163
            // only collect annotations we want
164
            foreach ($annotations as $annotation) {
165
                if ($annotation->name === $annotationType) {
166
                    $collectedAnnotations[] = $annotation;
167
                }
168
            }
169
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
170
        } else {
171
            // all other annotations we would like to parse ourselves
172
173
            $rawAnnotations = array();
174
            preg_match_all('/@' . $annotationType . '.+?\n/s', $string, $rawAnnotations);
175
176
            // build up stdClass instances from the result
177
            foreach ($rawAnnotations[0] as $rawAnnotation) {
178
                $annotationPieces = explode('##', preg_replace('/\s+/', '##', $rawAnnotation));
179
180
                // short sanity check
181
                if ($annotationPieces[0] === '@' . $annotationType &&
182
                    is_string($annotationPieces[1]) &&
183
                    (is_string($annotationPieces[2]) || $annotationType === 'return')
184
                ) {
185
                    // we got at least the pieces we are searching for, but we do not care about meaning here
186
187
                    // create the class and fill it
188
                    $annotation = new \stdClass();
189
                    $annotation->name = $annotationType;
190
                    $annotation->values = array(
191
                        'operand' => empty($annotationPieces[2]) ? '' : $annotationPieces[2],
192
                        'typeHint' => $annotationPieces[1]
193
                    );
194
195
                    $collectedAnnotations[] = $annotation;
196
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
197
                } else {
198
                    // tell them we got a problem
199
200
                    throw new ParserException(
201
                        sprintf(
202
                            'Could not parse annotation %s within structure %s',
203
                            $rawAnnotation,
204
                            $this->currentDefinition->getQualifiedName()
205
                        )
206
                    );
207
                }
208
            }
209
        }
210
211
        return $collectedAnnotations;
212
    }
213
214
    /**
215
     * Will return one pointcut which does specifically only match the joinpoints of the structure
216
     * which this docblock belongs to
217
     *
218
     * @param string $docBlock   The DocBlock to search in
219
     * @param string $targetType Type of the target any resulting joinpoints have, e.g. Joinpoint::TARGET_METHOD
220
     * @param string $targetName Name of the target any resulting joinpoints have
221
     *
222
     * @return \AppserverIo\Doppelgaenger\Entities\Lists\PointcutExpressionList
223
     */
224
    public function getPointcutExpressions($docBlock, $targetType, $targetName)
225
    {
226
        $pointcutExpressions = new PointcutExpressionList();
227
228
        // get our tokenizer and parse the doc Block
229
        $tokenizer = new Tokenizer();
230
231
        $tokenizer->ignore(
232
            array(
233
                'param',
234
                'return',
235
                'throws'
236
            )
237
        );
238
        $tokens = new Tokens($tokenizer->parse($docBlock));
239
240
        // convert to array and run it through our advice factory
241
        $toArray = new ToArray();
242
        $annotations = $toArray->convert($tokens);
243
244
        // create the entities for the join-points and advices the pointcut describes
245
        foreach ($annotations as $annotation) {
246
            // filter out the annotations which are no proper join-points
247
            if (!class_exists('\AppserverIo\Psr\MetaobjectProtocol\Aop\Annotations\Advices\\' . $annotation->name)) {
248
                continue;
249
            }
250
251
            // build the join-point
252
            $joinpoint = new Joinpoint();
253
            $joinpoint->setTarget($targetType);
254
            $joinpoint->setCodeHook($annotation->name);
255
            $joinpoint->setStructure($this->currentDefinition->getQualifiedName());
256
            $joinpoint->setTargetName($targetName);
257
258
            // build the pointcut(s)
259
            foreach ($annotation->values as $rawAdvice) {
260
                // as it might be an array we have to sanitize it first
261
                if (!is_array($rawAdvice)) {
262
                    $rawAdvice = array($rawAdvice);
263
                }
264
                foreach ($rawAdvice as $adviceString) {
265
                    // create the pointcut
266
                    $pointcutExpression = new PointcutExpression($adviceString);
267
                    $pointcutExpression->setJoinpoint($joinpoint);
268
269
                    $pointcutExpressions->add($pointcutExpression);
270
                }
271
            }
272
        }
273
274
        return $pointcutExpressions;
275
    }
276
277
    /**
278
     * Will get the conditions for a certain assertion indicating keyword like @requires or, if configured, @param
279
     *
280
     * @param string       $docBlock         The DocBlock to search in
281
     * @param string       $conditionKeyword The keyword we are searching for, use assertion defining tags here!
282
     * @param boolean|null $privateContext   If we have to mark the parsed annotations as having a private context
283
     *                                       as we would have trouble finding out for ourselves.
284
     *
285
     * @return boolean|\AppserverIo\Doppelgaenger\Entities\Lists\AssertionList
286
     */
287
    public function getConditions($docBlock, $conditionKeyword, $privateContext = null)
288
    {
289
        // There are only 3 valid condition types
290
        if ($conditionKeyword !== Requires::ANNOTATION && $conditionKeyword !== Ensures::ANNOTATION
291
            && $conditionKeyword !== Invariant::ANNOTATION
292
        ) {
293
            return false;
294
        }
295
296
        // get the annotations for the passed condition keyword
297
        $annotations = $this->getAnnotationsByType($docBlock, $conditionKeyword);
298
299
        // if we have to enforce basic type safety we need some more annotations
300
        if ($this->config->getValue('enforcement/enforce-default-type-safety') === true) {
301
            // lets switch the
302
303
            switch ($conditionKeyword) {
304
                case Ensures::ANNOTATION:
305
                    // we have to consider @return annotations as well
306
307
                    $annotations = array_merge(
308
                        $annotations,
309
                        $this->getAnnotationsByType($docBlock, 'return')
310
                    );
311
                    break;
312
313
                case Requires::ANNOTATION:
314
                    // we have to consider @param annotations as well
315
316
                    $annotations = array_merge(
317
                        $annotations,
318
                        $this->getAnnotationsByType($docBlock, 'param')
319
                    );
320
                    break;
321
322
                default:
323
                    break;
324
            }
325
        }
326
327
        // lets build up the result array
328
        $assertionFactory = new AssertionFactory();
329
        if ($this->currentDefinition) {
330
            $assertionFactory->setCurrentDefinition($this->currentDefinition);
331
        }
332
        $result = new AssertionList();
333
        foreach ($annotations as $annotation) {
334
            // try to create assertion instances for all annotations
335
            try {
336
                $assertion = $assertionFactory->getInstance($annotation);
337
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
338
            } catch (\Exception $e) {
339
                error_log($e->getMessage());
340
                continue;
341
            }
342
343
            if ($assertion !== false) {
344
                // Do we already got a private context we can set? If not we have to find out four ourselves
345
                if ($privateContext !== null) {
346
                    // Add the context (wether private or not)
347
                    $assertion->setPrivateContext($privateContext);
348
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
349
                } else {
350
                    // Add the context (private or not)
351
                    $this->determinePrivateContext($assertion);
0 ignored issues
show
Bug introduced by
It seems like $assertion defined by $assertionFactory->getInstance($annotation) on line 336 can be null; however, AppserverIo\Doppelgaenge...terminePrivateContext() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
352
                }
353
354
                // finally determine the minimal scope of this assertion and add it to our result
355
                $this->determineMinimalScope($assertion);
0 ignored issues
show
Bug introduced by
It seems like $assertion defined by $assertionFactory->getInstance($annotation) on line 336 can be null; however, AppserverIo\Doppelgaenge...determineMinimalScope() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
356
                $result->add($assertion);
357
            }
358
        }
359
360
        return $result;
361
    }
362
363
    /**
364
     * Will filter all method calls from within the assertion string
365
     *
366
     * @param string $docString The DocBlock piece to search in
367
     *
368
     * @return array
369
     */
370
    protected function filterMethodCalls($docString)
371
    {
372
        // We will be regex ninjas here
373
        $results = array();
374
        preg_match_all('/->(.*?)\(/', $docString, $results);
375
376
        // Return the clean output
377
        return $results[1];
378
    }
379
380
    /**
381
     * Will filter all attributes which are used within an assertion string
382
     *
383
     * @param string $docString The DocBlock piece to search in
384
     *
385
     * @return array
386
     */
387
    protected function filterAttributes($docString)
388
    {
389
        // We will be regex ninjas here
390
        $tmp = array();
391
        preg_match_all('/(this->|self::)([a-zA-Z0-9_]*?)[=!\s<>,\)\[\]]/', $docString, $tmp);
392
393
        $results = array();
394
        foreach ($tmp[2] as $rawAttribute) {
395
            $results[] = '$' . $rawAttribute;
396
        }
397
398
        // Return the clean output
399
        return $results;
400
    }
401
402
    /**
403
     * Will try to figure out if the passed assertion has a private context or not.
404
     * This information will be entered into the assertion which will then be returned.
405
     *
406
     * @param \AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface $assertion The assertion we need the context for
407
     *
408
     * @return void
409
     */
410
    protected function determinePrivateContext(AssertionInterface $assertion)
411
    {
412
        // we only have to act if the current definition has functions and properties
413
        if (!$this->currentDefinition instanceof PropertiedStructureInterface || !$this->currentDefinition instanceof StructureDefinitionInterface) {
414
            return;
415
        }
416
417
        // Get the string to check for dynamic properties
418
        $assertionString = $assertion->getString();
419
420
        // Do we have method calls?
421
        $methodCalls = $this->filterMethodCalls($assertionString);
422
423 View Code Duplication
        if (!empty($methodCalls)) {
424
            // Iterate over all method calls and check if they are private
425
            foreach ($methodCalls as $methodCall) {
426
                // Get the function definition, but do not get recursive conditions
427
                $functionDefinition = $this->currentDefinition->getFunctionDefinitions()->get($methodCall);
428
429
                // If we found something private we can end here
430
                if ($functionDefinition instanceof FunctionDefinition &&
431
                    $functionDefinition->getVisibility() === 'private'
432
                ) {
433
                    // Set the private context to true and return it
434
                    $assertion->setPrivateContext(true);
435
436
                    return;
437
                }
438
            }
439
        }
440
441
        // Do we have any attributes?
442
        $attributes = $this->filterAttributes($assertionString);
443
444 View Code Duplication
        if (!empty($attributes)) {
445
            // Iterate over all attributes and check if they are private
446
            foreach ($attributes as $attribute) {
447
                $attributeDefinition = $this->currentDefinition->getAttributeDefinitions()->get($attribute);
448
449
                // If we found something private we can end here
450
                if ($attributeDefinition instanceof AttributeDefinition &&
451
                    $attributeDefinition->getVisibility() === 'private'
452
                ) {
453
                    // Set the private context to true and return it
454
                    $assertion->setPrivateContext(true);
455
456
                    return;
457
                }
458
            }
459
        }
460
    }
461
462
    /**
463
     * Will try to figure out if the passed assertion has a private context or not.
464
     * This information will be entered into the assertion which will then be returned.
465
     *
466
     * @param \AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface $assertion The assertion we need the minimal scope for
467
     *
468
     * @return void
469
     */
470
    protected function determineMinimalScope(AssertionInterface $assertion)
471
    {
472
        // Get the string to check for dynamic properties
473
        $assertionString = $assertion->getString();
474
475
        // Do we have method calls? If so we have at least structure scope
476
        $methodCalls = $this->filterMethodCalls($assertionString);
477
        if (!empty($methodCalls)) {
478
            $assertion->setMinScope('structure');
479
        }
480
481
        // Do we have any attributes? If so we have at least structure scope
482
        $attributes = $this->filterAttributes($assertionString);
483
        if (!empty($attributes)) {
484
            $assertion->setMinScope('structure');
485
        }
486
    }
487
}
488