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