diff --git a/Build/Scripts/annotationChecker.php b/Build/Scripts/annotationChecker.php new file mode 100755 index 0000000000000000000000000000000000000000..a653056476556e107c60ececc8b6bbfbe8f3a83b --- /dev/null +++ b/Build/Scripts/annotationChecker.php @@ -0,0 +1,146 @@ +#!/usr/bin/env php +<?php +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use PhpParser\Comment\Doc; +use PhpParser\Error; +use PhpParser\Node; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitorAbstract; +use PhpParser\ParserFactory; +use Symfony\Component\Console\Output\ConsoleOutput; + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/** + * Class NodeVisitor + */ +class NodeVisitor extends NodeVisitorAbstract +{ + /** + * @var array + */ + public $matches = []; + + public function enterNode(Node $node) + { + switch (get_class($node)) { + case Node\Stmt\Class_::class: + case Node\Stmt\Property::class: + case Node\Stmt\ClassMethod::class: + /** Node\Stmt\ClassMethod $node */ + if (!($docComment = $node->getDocComment()) instanceof Doc) { + return; + } + + // These annotations are OK to have, everything else is denied + $negativeLookaheadMatches = [ + // Annotation tags + 'Annotation', 'Attribute', 'Attributes', 'Required', 'Target', + // Widely used tags (but not existent in phpdoc) + 'fix', 'fixme', 'override', + // PHPDocumentor 1 tags + 'abstract', 'access', 'code', 'deprec', 'endcode', 'exception', 'final', 'ingroup', 'inheritdoc', 'inheritDoc', 'magic', 'name', 'toc', 'tutorial', 'private', 'static', 'staticvar', 'staticVar', 'throw', + // PHPDocumentor 2 tags + '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', + // PHPUnit tags + 'codeCoverageIgnore', 'codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'test', 'covers', 'dataProvider', 'group', 'skip', 'depends', 'expectedException', 'before', + // PHPCheckStyle + 'SuppressWarnings', 'noinspection', + // Extbase related (deprecated) + 'inject', 'transient', 'lazy', 'validate', 'cascade', 'cli', 'flushesCaches', + // Extbase related + 'Extbase\\\\Inject', 'Inject', 'Transient', 'Extbase\\\\ORM\\\\Lazy', 'Cascade', 'IgnoreValidation', 'Enum', + // Extension scanner + 'extensionScannerIgnoreFile', 'extensionScannerIgnoreLine' + ]; + + $matches = []; + preg_match_all( + '/\*\s@(?!' . implode('|', $negativeLookaheadMatches) . ')(?<annotations>[a-zA-Z0-9\\\\]+)/', + $docComment->getText(), + $matches + ); + + if (!empty($matches['annotations'])) { + $this->matches[$node->getLine()] = array_map(function ($value) { + return '@' . $value; + }, $matches['annotations']); + } + + break; + default: + break; + } + } +} + +$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7); + +$finder = new Symfony\Component\Finder\Finder(); +$finder->files() + ->in(__DIR__ . '/../../typo3/') + ->name('/\.php$/') +; + +$output = new ConsoleOutput(); + +$errors = []; +foreach ($finder as $file) { + try { + $ast = $parser->parse($file->getContents()); + } catch (Error $error) { + $output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>'); + exit(1); + } + + $visitor = new NodeVisitor(); + + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + + $ast = $traverser->traverse($ast); + + if (!empty($visitor->matches)) { + $errors[$file->getRealPath()] = $visitor->matches; + $output->write('<error>F</error>'); + } else { + $output->write('<fg=green>.</>'); + } +} + +$output->writeln(''); + +if (!empty($errors)) { + $output->writeln(''); + + foreach ($errors as $file => $matchesPerLine) { + $output->writeln(''); + $output->writeln('<error>' . $file . '</error>'); + + /** + * @var array $matchesPerLine + * @var int $line + * @var array $matches + */ + foreach ($matchesPerLine as $line => $matches) { + $output->writeln($line . ': ' . implode(', ', $matches)); + } + } + exit(1); +} + +exit(0); diff --git a/Build/bamboo/src/main/java/core/AbstractCoreSpec.java b/Build/bamboo/src/main/java/core/AbstractCoreSpec.java index d6b04f1064be3a099d4d498aa7066945d8b5e2f1..ad95c007d6b1f381f3a734f86292cbc7fc31a7c3 100644 --- a/Build/bamboo/src/main/java/core/AbstractCoreSpec.java +++ b/Build/bamboo/src/main/java/core/AbstractCoreSpec.java @@ -421,6 +421,31 @@ abstract public class AbstractCoreSpec { return jobs; } + /** + * Job with integration test checking for valid @xy annotations + */ + protected Job getJobIntegrationAnnotations() { + return new Job("Integration annotations", new BambooKey("IANNO")) + .description("Check docblock-annotations by executing Build/Scripts/annotationChecker.php script") + .pluginConfigurations(this.getDefaultJobPluginConfiguration()) + .tasks( + this.getTaskGitCloneRepository(), + this.getTaskGitCherryPick(), + this.getTaskComposerInstall(), + new ScriptTask() + .description("Execute annotations check script") + .interpreter(ScriptTaskProperties.Interpreter.BINSH_OR_CMDEXE) + .inlineBody( + this.getScriptTaskBashInlineBody() + + "./Build/Scripts/annotationChecker.php\n" + ) + ) + .requirements( + this.getRequirementPhpVersion72() + ) + .cleanWorkingDirectory(true); + } + /** * Job with various smaller script tests */ diff --git a/Build/bamboo/src/main/java/core/NightlySpec.java b/Build/bamboo/src/main/java/core/NightlySpec.java index 2fc7b9388ad1870dd67107920a3475d0bc2e5ca8..739f52e30c105ab562d629977f26336bee24b6da 100644 --- a/Build/bamboo/src/main/java/core/NightlySpec.java +++ b/Build/bamboo/src/main/java/core/NightlySpec.java @@ -85,6 +85,8 @@ public class NightlySpec extends AbstractCoreSpec { jobsMainStage.add(this.getJobCglCheckFullCore()); + jobsMainStage.add(this.getJobIntegrationAnnotations()); + jobsMainStage.add(this.getJobIntegrationVarious()); jobsMainStage.addAll(this.getJobsFunctionalTestsMysql(this.numberOfFunctionalMysqlJobs, this.getRequirementPhpVersion72(), "PHP72")); diff --git a/Build/bamboo/src/main/java/core/PreMergeSpec.java b/Build/bamboo/src/main/java/core/PreMergeSpec.java index 59807d3cefd5ed372df52ce244eb51e4aeb8fb9f..fb14491ef175bbba6d702f133a6e158a4f2f80b3 100644 --- a/Build/bamboo/src/main/java/core/PreMergeSpec.java +++ b/Build/bamboo/src/main/java/core/PreMergeSpec.java @@ -93,6 +93,8 @@ public class PreMergeSpec extends AbstractCoreSpec { jobsMainStage.addAll(this.getJobsAcceptanceTestsMysql(this.numberOfAcceptanceTestJobs, this.getRequirementPhpVersion72(), "PHP72")); + jobsMainStage.add(this.getJobIntegrationAnnotations()); + jobsMainStage.add(this.getJobIntegrationVarious()); jobsMainStage.addAll(this.getJobsFunctionalTestsMysql(this.numberOfFunctionalMysqlJobs, this.getRequirementPhpVersion72(), "PHP72"));