‪TYPO3CMS  ‪main
annotationChecker.php
Go to the documentation of this file.
1 #!/usr/bin/env php
2 <?php
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 use PhpParser\Comment\Doc;
19 use PhpParser\Error;
20 use PhpParser\Node;
21 use PhpParser\NodeTraverser;
22 use PhpParser\NodeVisitorAbstract;
23 use PhpParser\ParserFactory;
24 use Symfony\Component\Console\Output\ConsoleOutput;
25 
26 require_once __DIR__ . '/../../vendor/autoload.php';
27 
28 class NodeVisitor extends NodeVisitorAbstract
29 {
33  public array ‪$matches = [];
34 
35  public function ‪enterNode(Node $node)
36  {
37  switch (get_class($node)) {
38  case Node\Stmt\Class_::class:
39  case Node\Stmt\Property::class:
40  case Node\Stmt\ClassMethod::class:
42  if (!($docComment = $node->getDocComment()) instanceof Doc) {
43  return;
44  }
45 
46  // These annotations are OK to have on class, class property and class method level, everything else is denied
47  $negativeLookaheadMatches = [
48  // Annotation tags
49  'Annotation', 'Attribute', 'Attributes', 'Required', 'Target',
50  // Widely used tags (but not existent in phpdoc)
51  'fix', 'fixme', 'override',
52  // PHPDocumentor 1 tags
53  'abstract', 'code', 'deprec', 'endcode', 'exception', 'final', 'ingroup', 'magic', 'name', 'toc', 'tutorial', 'private', 'static', 'staticvar', 'staticVar', 'throw',
54  // PHPDocumentor 2 tags
55  'api', 'author', 'category', 'copyright', 'deprecated', 'example', 'filesource', 'global', 'ignore', 'internal', 'license', 'link', 'method', 'package', 'param', 'property', 'property-read', 'property-write', 'return', 'see', 'since', 'source', 'subpackage', 'throws', 'todo', 'TODO', 'usedby', 'uses', 'var', 'version',
56  // PHPUnit tags
57  'codeCoverageIgnore', 'codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'test', 'dataProvider', 'group', 'skip', 'depends', 'expectedException', 'before', 'requires',
58  // codeception tags
59  'env',
60  // PHPCheckStyle
61  'SuppressWarnings', 'noinspection',
62  // Extbase related
63  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\IgnoreValidation', 'Extbase\\\\IgnoreValidation', 'IgnoreValidation',
64  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\Inject', 'Extbase\\\\Inject', 'Inject',
65  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\Validate', 'Extbase\\\\Validate', 'Validate',
66  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Cascade', 'Extbase\\\\ORM\\\\Cascade', 'Cascade',
67  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Lazy', 'Extbase\\\\ORM\\\\Lazy', 'Lazy',
68  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Transient', 'Extbase\\\\ORM\\\\Transient', 'Transient',
69  // annotations shipped with doctrine/annotations
70  'Doctrine\\\\Common\\\\Annotations\\\\Annotation\\\\Enum', 'Enum',
71  // Extension scanner
72  'extensionScannerIgnoreFile', 'extensionScannerIgnoreLine',
73  // static code analysis
74  'template', 'implements', 'extends',
75  // phpstan specific annotations
76  'phpstan-var', 'phpstan-param', 'phpstan-return',
77  ];
78  // allow annotation only on class level
79  if (get_class($node) === Node\Stmt\Class_::class) {
80  $negativeLookaheadMatches = array_merge(
81  $negativeLookaheadMatches,
82  [
83  // PHPStan
84  'phpstan-type', 'phpstan-import-type',
85  ]
86  );
87  }
88 
89  ‪$matches = [];
90  preg_match_all(
91  '/\*(\s+)@(?!' . implode('|', $negativeLookaheadMatches) . ')(?<annotations>[a-zA-Z0-9\-\\\\]+)/',
92  $docComment->getText(),
94  );
95  if (!empty(‪$matches['annotations'])) {
96  $this->matches[$node->getLine()] = array_map(static function (string $value): string {
97  return '@' . $value;
98  }, ‪$matches['annotations']);
99  }
100 
101  break;
102  default:
103  break;
104  }
105  }
106 }
107 
108 ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
109 
110 ‪$finder = new Symfony\Component\Finder\Finder();
111 ‪$finder->files()
112  ->in(__DIR__ . '/../../typo3/')
113  ->name('/\.php$/')
114  // black list some unit test fixture files from extension scanner that test matchers of old annotations
115  ->notName('MethodAnnotationMatcherFixture.php')
116  ->notName('PropertyAnnotationMatcherFixture.php')
117 ;
118 
119 ‪$output = new ConsoleOutput();
120 
122 foreach (‪$finder as $file) {
123  try {
124  $ast = ‪$parser->parse($file->getContents());
125  } catch (Error $error) {
126  ‪$output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>');
127  exit(1);
128  }
129 
130  $visitor = new NodeVisitor();
131 
132  $traverser = new NodeTraverser();
133  $traverser->addVisitor($visitor);
134 
135  $ast = $traverser->traverse($ast);
136 
137  if (!empty($visitor->matches)) {
138  ‪$errors[$file->getRealPath()] = $visitor->matches;
139  ‪$output->write('<error>F</error>');
140  } else {
141  ‪$output->write('<fg=green>.</>');
142  }
143 }
144 
145 ‪$output->writeln('');
146 
147 if (!empty(‪$errors)) {
148  ‪$output->writeln('');
149 
150  foreach (‪$errors as $file => $matchesPerLine) {
151  ‪$output->writeln('');
152  ‪$output->writeln('<error>' . $file . '</error>');
153 
159  foreach ($matchesPerLine as $line => $matches) {
160  ‪$output->writeln($line . ': ' . implode(', ', $matches));
161  }
162  }
163  exit(1);
164 }
165 
166 exit(0);
‪NodeVisitor\enterNode
‪enterNode(Node $node)
Definition: annotationChecker.php:35
‪$finder
‪$finder
Definition: annotationChecker.php:110
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪$errors
‪$errors
Definition: annotationChecker.php:121
‪$output
‪$output
Definition: annotationChecker.php:119
‪NodeVisitor\$matches
‪array $matches
Definition: annotationChecker.php:33