‪TYPO3CMS  10.4
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  ];
79  // allow annotation only on class level
80  if (get_class($node) === Node\Stmt\Class_::class) {
81  $negativeLookaheadMatches = array_merge(
82  $negativeLookaheadMatches,
83  [
84  // PHPStan
85  'phpstan-type', 'phpstan-import-type',
86  ]
87  );
88  }
89 
90  ‪$matches = [];
91  preg_match_all(
92  '/\*(\s+)@(?!' . implode('|', $negativeLookaheadMatches) . ')(?<annotations>[a-zA-Z0-9\-\\\\]+)/',
93  $docComment->getText(),
95  );
96  if (!empty(‪$matches['annotations'])) {
97  $this->matches[$node->getLine()] = array_map(function ($value) {
98  return '@' . $value;
99  }, ‪$matches['annotations']);
100  }
101 
102  break;
103  default:
104  break;
105  }
106  }
107 }
108 
109 ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
110 
111 ‪$finder = new Symfony\Component\Finder\Finder();
112 ‪$finder->files()
113  ->in(__DIR__ . '/../../typo3/')
114  ->name('/\.php$/')
115  // black list some unit test fixture files from extension scanner that test matchers of old annotations
116  ->notName('MethodAnnotationMatcherFixture.php')
117  ->notName('PropertyAnnotationMatcherFixture.php')
118 ;
119 
120 ‪$output = new ConsoleOutput();
121 
122 ‪$errors = [];
123 foreach (‪$finder as $file) {
124  try {
125  $ast = ‪$parser->parse($file->getContents());
126  } catch (Error $error) {
127  ‪$output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>');
128  exit(1);
129  }
130 
131  $visitor = new NodeVisitor();
132 
133  $traverser = new NodeTraverser();
134  $traverser->addVisitor($visitor);
135 
136  $ast = $traverser->traverse($ast);
137 
138  if (!empty($visitor->matches)) {
139  ‪$errors[$file->getRealPath()] = $visitor->matches;
140  ‪$output->write('<error>F</error>');
141  } else {
142  ‪$output->write('<fg=green>.</>');
143  }
144 }
145 
146 ‪$output->writeln('');
147 
148 if (!empty(‪$errors)) {
149  ‪$output->writeln('');
150 
151  foreach (‪$errors as $file => $matchesPerLine) {
152  ‪$output->writeln('');
153  ‪$output->writeln('<error>' . $file . '</error>');
154 
160  foreach ($matchesPerLine as $line => $matches) {
161  ‪$output->writeln($line . ': ' . implode(', ', $matches));
162  }
163  }
164  exit(1);
165 }
166 
167 exit(0);
‪NodeVisitor\enterNode
‪enterNode(Node $node)
Definition: annotationChecker.php:37
‪$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:35