Commit d9f4eb0f authored by Alexander Schnitzler's avatar Alexander Schnitzler Committed by Christian Kuhn
Browse files

[TASK] Script to check doc comments for invalid annotations

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: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent c514a836
#!/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);
......@@ -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
*/
......
......@@ -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"));
......
......@@ -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"));
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment