‪TYPO3CMS  11.5
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 
31 class NodeVisitor extends NodeVisitorAbstract
32 {
36  public ‪$matches = [];
37 
38  public function ‪enterNode(Node $node)
39  {
40  switch (get_class($node)) {
41  case Node\Stmt\Class_::class:
42  case Node\Stmt\Property::class:
43  case Node\Stmt\ClassMethod::class:
45  if (!($docComment = $node->getDocComment()) instanceof Doc) {
46  return;
47  }
48 
49  // These annotations are OK to have on class, class property and class method level, everything else is denied
50  $negativeLookaheadMatches = [
51  // Annotation tags
52  'Annotation', 'Attribute', 'Attributes', 'Required', 'Target',
53  // Widely used tags (but not existent in phpdoc)
54  'fix', 'fixme', 'override',
55  // PHPDocumentor 1 tags
56  'abstract', 'code', 'deprec', 'endcode', 'exception', 'final', 'ingroup', 'inheritdoc', 'inheritDoc', 'magic', 'name', 'toc', 'tutorial', 'private', 'static', 'staticvar', 'staticVar', 'throw',
57  // PHPDocumentor 2 tags
58  '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',
59  // PHPUnit tags
60  'codeCoverageIgnore', 'codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'test', 'covers', 'dataProvider', 'group', 'skip', 'depends', 'expectedException', 'before', 'requires',
61  // codeception tags
62  'env',
63  // PHPCheckStyle
64  'SuppressWarnings', 'noinspection',
65  // Extbase related
66  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\IgnoreValidation', 'Extbase\\\\IgnoreValidation', 'IgnoreValidation',
67  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\Inject', 'Extbase\\\\Inject', 'Inject',
68  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\Validate', 'Extbase\\\\Validate', 'Validate',
69  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Cascade', 'Extbase\\\\ORM\\\\Cascade', 'Cascade',
70  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Lazy', 'Extbase\\\\ORM\\\\Lazy', 'Lazy',
71  'TYPO3\\\\CMS\\\\Extbase\\\\Annotation\\\\ORM\\\\Transient', 'Extbase\\\\ORM\\\\Transient', 'Transient',
72  // annotations shipped with doctrine/annotations
73  'Doctrine\\\\Common\\\\Annotations\\\\Annotation\\\\Enum', 'Enum',
74  // Extension scanner
75  'extensionScannerIgnoreFile', 'extensionScannerIgnoreLine',
76  // static code analysis
77  'template', 'implements', 'extends',
78  // phpstan specific annotations
79  'phpstan-var', 'phpstan-param', 'phpstan-return',
80  ];
81  // allow annotation only on class level
82  if (get_class($node) === Node\Stmt\Class_::class) {
83  $negativeLookaheadMatches = array_merge(
84  $negativeLookaheadMatches,
85  [
86  // PHPStan
87  'phpstan-type', 'phpstan-import-type',
88  ]
89  );
90  }
91 
92  ‪$matches = [];
93  preg_match_all(
94  '/\*(\s+)@(?!' . implode('|', $negativeLookaheadMatches) . ')(?<annotations>[a-zA-Z0-9\-\\\\]+)/',
95  $docComment->getText(),
97  );
98  if (!empty(‪$matches['annotations'])) {
99  $this->matches[$node->getLine()] = array_map(static function ($value) {
100  return '@' . $value;
101  }, ‪$matches['annotations']);
102  }
103 
104  break;
105  default:
106  break;
107  }
108  }
109 }
110 
111 ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
112 
113 ‪$finder = new Symfony\Component\Finder\Finder();
114 ‪$finder->files()
115  ->in(__DIR__ . '/../../typo3/')
116  ->name('/\.php$/')
117  // black list some unit test fixture files from extension scanner that test matchers of old annotations
118  ->notName('MethodAnnotationMatcherFixture.php')
119  ->notName('PropertyAnnotationMatcherFixture.php')
120 ;
121 
122 ‪$output = new ConsoleOutput();
123 
124 ‪$errors = [];
125 foreach (‪$finder as $file) {
126  try {
127  $ast = ‪$parser->parse($file->getContents());
128  } catch (Error $error) {
129  ‪$output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>');
130  exit(1);
131  }
132 
133  $visitor = new NodeVisitor();
134 
135  $traverser = new NodeTraverser();
136  $traverser->addVisitor($visitor);
137 
138  $ast = $traverser->traverse($ast);
139 
140  if (!empty($visitor->matches)) {
141  ‪$errors[$file->getRealPath()] = $visitor->matches;
142  ‪$output->write('<error>F</error>');
143  } else {
144  ‪$output->write('<fg=green>.</>');
145  }
146 }
147 
148 ‪$output->writeln('');
149 
150 if (!empty(‪$errors)) {
151  ‪$output->writeln('');
152 
153  foreach (‪$errors as $file => $matchesPerLine) {
154  ‪$output->writeln('');
155  ‪$output->writeln('<error>' . $file . '</error>');
156 
162  foreach ($matchesPerLine as $line => $matches) {
163  ‪$output->writeln($line . ': ' . implode(', ', $matches));
164  }
165  }
166  exit(1);
167 }
168 
169 exit(0);
‪NodeVisitor\enterNode
‪enterNode(Node $node)
Definition: annotationChecker.php:37
‪$finder
‪$finder
Definition: annotationChecker.php:112
‪$parser
‪$parser
Definition: annotationChecker.php:110
‪$errors
‪$errors
Definition: annotationChecker.php:123
‪$output
‪$output
Definition: annotationChecker.php:121
‪NodeVisitor\$matches
‪array $matches
Definition: annotationChecker.php:35