[TASK] Script to check doc comments for invalid annotations 90/54790/11
authorAlexander Schnitzler <git@alexanderschnitzler.de>
Mon, 27 Nov 2017 13:16:36 +0000 (14:16 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Thu, 14 Dec 2017 20:20:29 +0000 (21:20 +0100)
To prevent the introduction of further invalid
php doc annotations a build script should scan
all php files and report the usage of invalid
annotations.

Releases: master
Resolves: #83115
Change-Id: I56cc64ef43037c6c55f5337d07cf722a1927865c
Reviewed-on: https://review.typo3.org/54790
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Build/Scripts/annotationChecker.php [new file with mode: 0755]
Build/bamboo/src/main/java/core/AbstractCoreSpec.java
Build/bamboo/src/main/java/core/NightlySpec.java
Build/bamboo/src/main/java/core/PreMergeSpec.java

diff --git a/Build/Scripts/annotationChecker.php b/Build/Scripts/annotationChecker.php
new file mode 100755 (executable)
index 0000000..a653056
--- /dev/null
@@ -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);
index d6b04f1..ad95c00 100644 (file)
@@ -422,6 +422,31 @@ abstract public class AbstractCoreSpec {
     }
 
     /**
+     * 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
      */
     protected Job getJobIntegrationVarious() {
index 2fc7b93..739f52e 100644 (file)
@@ -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"));
index 59807d3..fb14491 100644 (file)
@@ -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"));