Skip to content

Instantly share code, notes, and snippets.

@inxilpro
Created September 15, 2023 13:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inxilpro/0c295cf11e09f371bf2cc32e426a65cd to your computer and use it in GitHub Desktop.
Save inxilpro/0c295cf11e09f371bf2cc32e426a65cd to your computer and use it in GitHub Desktop.
<?php
namespace Tests\Project;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Tests\Constraints;
use Tests\TestCase;
class FixMeTest extends TestCase
{
protected $exclude = [
'.git',
'.idea',
'docs',
'node_modules',
'public',
'storage',
'vendor',
'tests/Project/FixMeTest.php',
'tests/Constraints/HasNoFixmes.php',
];
protected $extensions = [
'.php',
'.blade.php',
'.js',
'.jsx',
'.less',
];
protected $baseDirectory;
protected function setUp(): void
{
parent::setUp();
foreach ($this->exclude as $key => $value) {
$this->exclude[$key] = $this->app->basePath($value);
}
}
public function test_project_has_no_fixme_comments(): void
{
$directory_iterator = new RecursiveDirectoryIterator(
$this->app->basePath(),
RecursiveDirectoryIterator::SKIP_DOTS
);
$filtered_iterator = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator($directory_iterator, function(SplFileInfo $file, $key, RecursiveDirectoryIterator $iterator) {
// Exclude root directories
if (starts_with($file->getPathname(), $this->exclude)) {
return false;
}
// Allow iteration
if ($iterator->hasChildren()) {
return true;
}
// Only include our file types
if (! ends_with($file->getFilename(), $this->extensions)) {
return false;
}
return $file->isFile();
})
);
$this->assertThat($filtered_iterator, new Constraints\HasNoFixmes($this->app->basePath()));
}
}
<?php
namespace Tests\Constraints;
use PHPUnit\Framework\Constraint\Constraint;
class HasNoFixmes extends Constraint
{
protected $base_path;
protected $fixme_comments = [];
protected $ran = false;
public function __construct(string $base_path)
{
$this->base_path = $base_path;
}
public function toString(): string
{
return 'has no FIXME comments';
}
protected function matches($iterator): bool
{
if (! $this->ran) {
foreach ($iterator as $pathname => $file_info) {
$content = file_get_contents($pathname);
if (stripos($content, 'fixme') === false) {
continue;
}
$comments = stripos($pathname, '.php') === false
? $this->readGenericComments($content)
: $this->readPhpComments($content);
if (count($comments)) {
$this->fixme_comments[$pathname] = (object) [
'filename' => str_replace($this->base_path, '', $pathname),
'comments' => $comments,
];
}
}
$this->ran = true;
}
return empty($this->fixme_comments);
}
protected function readPhpComments(string $code): array
{
$tokens = token_get_all($code);
return collect($tokens)
->filter(function($token) {
return is_array($token);
})
->map(function($token) {
return (object) [
'type' => $token[0],
'source_code' => $token[1],
'line_number' => $token[2],
];
})
->filter(function($token) {
return T_COMMENT === $token->type || T_DOC_COMMENT === $token->type;
})
->filter(function($token) {
return stripos($token->source_code, 'fixme') !== false;
})
->map(function($token) {
$trim_syntax = '~(^\s*(?://\s?|/\*\s?|\*\s)|\*/\s*$|FIXME:?\s?)~m';
$comment = trim(preg_replace($trim_syntax, '', $token->source_code));
return (object) [
'line_number' => $token->line_number,
'comment' => $comment,
];
})
->toArray();
}
protected function readGenericComments(string $code): array
{
return collect(explode(PHP_EOL, $code))
->map(function($code, $line_number) {
return (object) [
'source_code' => (string) $code,
'line_number' => $line_number,
];
})
->filter(function($line) {
return stripos($line->source_code, 'fixme') !== false;
})
->map(function($line) {
$trim_syntax = '~(^\s*(?://\s?|/\*\s?|\*\s)|\*/\s*$|FIXME:?\s?)~m';
$comment = trim(preg_replace($trim_syntax, '', $line->source_code));
return (object) [
'line_number' => $line->line_number,
'comment' => (string) $comment,
];
})
->toArray();
}
protected function failureDescription($other): string
{
return 'the project has no FIXME comments';
}
protected function additionalFailureDescription($other): string
{
return collect($this->fixme_comments)
->map(function($file) {
$comments = collect($file->comments)
->map(function($comment) {
$text = str_replace("\n", ' ', trim($comment->comment));
return empty($text)
? "Line {$comment->line_number}"
: "{$comment->line_number}: {$text}";
});
if (1 === count($comments)) {
return " - {$file->filename}: {$comments->first()}";
}
return " - {$file->filename}\n + ".$comments->implode("\n + ");
})
->implode("\n");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment