-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEW Wrap PHP Code Sniffer with a markdown parser
- Loading branch information
1 parent
512a059
commit a8ae40f
Showing
5 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/vendor/ | ||
/composer.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
#!/usr/bin/env php | ||
<?php | ||
|
||
use PHP_CodeSniffer\Config; | ||
use PHP_CodeSniffer\Exceptions\DeepExitException; | ||
use SilverStripe\MD_PHP_CodeSniffer\Sniffer; | ||
|
||
// php_codesniffer autoloader (which itself includes the composer autoloader) | ||
$autoloadCandidates = [ | ||
// running from vendor/bin (recommended) | ||
__DIR__ . '/../squizlabs/php_codesniffer/autoload.php', | ||
// running from package in vendor/silverstripe/markdown-php-codesniffer/bin | ||
__DIR__ . '/../../../squizlabs/php_codesniffer/autoload.php', | ||
// running from package itself as root (used during development of this package itself) | ||
__DIR__ . '/../vendor/squizlabs/php_codesniffer/autoload.php', | ||
]; | ||
|
||
$autoloaded = false; | ||
foreach ($autoloadCandidates as $candidate) { | ||
if (is_file($candidate)) { | ||
require_once $candidate; | ||
$autoloaded = true; | ||
break; | ||
} | ||
} | ||
|
||
if (!$autoloaded) { | ||
die('Failed to include autoloader, unable to continue'); | ||
} | ||
|
||
$args = $_SERVER['argv']; | ||
$numArgs = count($args); | ||
$removeArgs = []; | ||
|
||
$usingExplicitStandard = false; | ||
|
||
// Check for any args we need from the CLI input | ||
for ($i = 0; $i < $numArgs; $i++) { | ||
$arg = $args[$i]; | ||
if ($arg === '--lint-language') { | ||
$lintLanguage = strtoupper($args[$i + 1]); | ||
$removeArgs[] = $i; | ||
$removeArgs[] = $i + 1; | ||
} elseif (str_starts_with($arg, '--lint-language=')) { | ||
$lintLanguage = str_replace('--lint-language=', '', $arg); | ||
$removeArgs[] = $i; | ||
} | ||
|
||
if ($arg === '--standard' || str_starts_with($arg, '--standard=')) { | ||
$usingExplicitStandard = true; | ||
} | ||
} | ||
|
||
// Remove our args and reset array indices so phpcs can accurately fetch its own args | ||
foreach ($removeArgs as $i) { | ||
unset($_SERVER['argv'][$i]); | ||
} | ||
$_SERVER['argv'] = array_values($_SERVER['argv']); | ||
|
||
// Assume PHP if no explicit language was passed in. | ||
$lintLanguage ??= 'PHP'; | ||
|
||
// PHPCS will just exist silently, so it's up to use to tell the user that their stuff won't | ||
// be properly linted | ||
if ($lintLanguage === 'JS' && !Config::getExecutablePath('eslint')) { | ||
throw new RuntimeException('No eslint executable found'); | ||
} | ||
|
||
try { | ||
$sniffer = new Sniffer(); | ||
$exitCode = $sniffer->run($lintLanguage, $usingExplicitStandard); | ||
exit($exitCode); | ||
} catch (DeepExitException $e) { | ||
echo $e->getMessage(); | ||
exit($e->getCode()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "silverstripe/markdown-php-codesniffer", | ||
"require": { | ||
"squizlabs/php_codesniffer": "^3.7", | ||
"league/commonmark": "^2.4" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"SilverStripe\\MD_PHP_CodeSniffer\\": "src/" | ||
} | ||
}, | ||
"bin": [ | ||
"bin/mdphpcs" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?xml version="1.0"?> | ||
<ruleset name="Markdown with PSR12"> | ||
<description>PSR12 pared down to what's sensible for code blocks in markdown</description> | ||
<rule ref="PSR12"> | ||
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/> | ||
<exclude name="PSR12.Files.FileHeader.HeaderPosition"/> | ||
<exclude name="PSR12.Files.FileHeader.SpacingAfterBlock"/> | ||
</rule> | ||
</ruleset> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
<?php | ||
|
||
namespace SilverStripe\MD_PHP_CodeSniffer; | ||
|
||
use League\CommonMark\Environment\Environment; | ||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; | ||
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; | ||
use League\CommonMark\Node\Query; | ||
use League\CommonMark\Parser\MarkdownParser; | ||
use PHP_CodeSniffer\Config; | ||
use PHP_CodeSniffer\Exceptions\DeepExitException; | ||
use PHP_CodeSniffer\Files\DummyFile; | ||
use PHP_CodeSniffer\Files\FileList; | ||
use PHP_CodeSniffer\Reporter; | ||
use PHP_CodeSniffer\Runner; | ||
use PHP_CodeSniffer\Util\Common; | ||
|
||
final class Sniffer | ||
{ | ||
private Query $query; | ||
|
||
private MarkdownParser $parser; | ||
|
||
public function __construct() | ||
{ | ||
$this->query = new Query(); | ||
$this->query->where(Query::type(FencedCode::class)); | ||
$environment = new Environment(); | ||
$environment->addExtension(new CommonMarkCoreExtension()); | ||
$this->parser = new MarkdownParser($environment); | ||
} | ||
|
||
public function run(string $lintLanguage, bool $usingExplicitStandard = false): int | ||
{ | ||
// Prevents errors when unexpected args are passed in, and forces fixing OFF | ||
if (!defined('PHP_CODESNIFFER_CBF')) { | ||
define('PHP_CODESNIFFER_CBF', false); | ||
} | ||
|
||
$sniffer = new Runner(); | ||
$sniffer->checkRequirements(); | ||
$sniffer->config = $this->prepareConfig($usingExplicitStandard, $lintLanguage); | ||
$sniffer->init(); | ||
$sniffer->reporter = new Reporter($sniffer->config); | ||
|
||
// Find all the relevant code blocks for linting | ||
// $files = $this->findMarkdownFiles($sniffer->config, $sniffer->ruleset); | ||
if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
echo 'Finding markdown files... '; | ||
} | ||
|
||
$files = new FileList($sniffer->config, $sniffer->ruleset); | ||
|
||
if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
$numFiles = count($files); | ||
echo "DONE ($numFiles files in queue)" . PHP_EOL; | ||
} | ||
|
||
$codeBlocks = $this->findFencedCodeblocks($files, $lintLanguage); | ||
|
||
// Add code blocks to the file list for linting | ||
$todo = []; | ||
foreach ($codeBlocks as $block) { | ||
$dummy = new DummyFile($block['content'], $sniffer->ruleset, $sniffer->config); | ||
$dummy->path = $block['path']; | ||
$todo[$dummy->path] = $dummy; | ||
} | ||
|
||
// Do the actual linting | ||
$numErrors = $this->sniff($sniffer, $todo); | ||
$sniffer->reporter->printReports(); | ||
|
||
if ($numErrors === 0) { | ||
// No errors found. | ||
return 0; | ||
/* we can't fix errors directly yet. | ||
} else if ($sniffer->reporter->totalFixable === 0) { | ||
// Errors found, but none of them can be fixed by PHPCBF. | ||
return 1; | ||
} else { | ||
// Errors found, and some can be fixed by PHPCBF. | ||
return 2; | ||
*/ | ||
} | ||
return 1; | ||
} | ||
|
||
private function prepareConfig(bool $usingExplicitStandard, string $lintLanguage): Config | ||
{ | ||
// Creating the Config object populates it with all required settings based on the phpcs/phpcbf CLI arguments provided. | ||
$config = new Config(); | ||
|
||
// We don't support STDIN for passing markdown in | ||
if ($config->stdin === true) { | ||
// 3 is the exit code phpcs uses for errors like this | ||
throw new DeepExitException('STDIN isn\'t supported', 3); | ||
} | ||
|
||
// Ensure we can find and lint markdown files | ||
$config->extensions = array_merge($config->extensions, ['md' => $lintLanguage]); | ||
// We're not passing the sniffer any real files, so caching could be unreliable | ||
$config->cache = false; | ||
// We must sniff all "files" sequentially - asyncronous sniffing isn't supported | ||
$config->parallel = 1; | ||
|
||
// If the user hasn't defined an explicit standard, and there's no default standards file to use, | ||
// use our customised PSR12 standard | ||
if (!$usingExplicitStandard && $config->standards === ['PEAR']) { | ||
$config->standards = [__DIR__ . '/../phpcs.default.xml']; | ||
} | ||
|
||
return $config; | ||
} | ||
|
||
/** | ||
* Finds all fenced codeblocks for the relevant language in all the markdown files | ||
*/ | ||
private function findFencedCodeblocks(FileList $paths, string $lintLanguage): array | ||
{ | ||
if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
echo 'Finding fenced codeblocks... '; | ||
} | ||
|
||
$blocks = []; | ||
|
||
/** @var string $path */ | ||
foreach ($paths as $path => $v) { | ||
$document = $this->parser->parse(file_get_contents($path)); | ||
$codeBlocks = $this->query->findAll($document); | ||
|
||
$n = 0; | ||
/** @var FencedCode $block */ | ||
foreach ($codeBlocks as $block) { | ||
if (strtoupper($block->getInfo()) !== $lintLanguage) { | ||
continue; | ||
} | ||
// We only want to count relevant code blocks | ||
$n++; | ||
|
||
// $startAt is the line in the md file where the ```php line sits | ||
$startAt = $block->getStartLine(); | ||
|
||
// Pad the content out so we have an accurate line count, and prepend a php code opening tag | ||
$content = str_repeat(PHP_EOL, $startAt - 1); | ||
$content .= '<?php' . PHP_EOL . $block->getLiteral(); | ||
|
||
// Report each block separately (by making the path unique) so it's treated as its own file | ||
// This lets us lint for things like namespaces more easily | ||
$blocks[] = [ | ||
'content' => $content, | ||
'path' => dirname($path) . '/' . basename($path, '.md') . "_{$n}" . '.md', | ||
]; | ||
} | ||
} | ||
|
||
if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
$numBlocks = count($blocks); | ||
echo "DONE ($numBlocks codeblocks in queue)" . PHP_EOL; | ||
} | ||
|
||
return $blocks; | ||
} | ||
|
||
/** | ||
* Run the codesniffing rules over the identified markdown codeblocks | ||
* | ||
* This is very nearly a direct copy of Runner::run() | ||
*/ | ||
private function sniff(Runner $sniffer, array $todo): int | ||
{ | ||
// Turn all sniff errors into exceptions. | ||
set_error_handler([$sniffer, 'handleErrors']); | ||
|
||
$lastDir = ''; | ||
$numFiles = count($todo); | ||
|
||
// Process each "file" sequentially - running sniff in parallel isn't supported | ||
// We're not actually running this across real files, but we should give the same output we'd get if we were. | ||
$numProcessed = 0; | ||
foreach ($todo as $path => $file) { | ||
if ($file->ignored === false) { | ||
$currDir = dirname($path); | ||
if ($lastDir !== $currDir) { | ||
if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
echo 'Changing into directory ' . Common::stripBasepath($currDir, $sniffer->config->basepath) . PHP_EOL; | ||
} | ||
|
||
$lastDir = $currDir; | ||
} | ||
|
||
$sniffer->processFile($file); | ||
} else if (PHP_CODESNIFFER_VERBOSITY > 0) { | ||
echo 'Skipping ' . basename($file->path) . PHP_EOL; | ||
} | ||
|
||
$numProcessed++; | ||
$sniffer->printProgress($file, $numFiles, $numProcessed); | ||
} | ||
|
||
restore_error_handler(); | ||
|
||
if (PHP_CODESNIFFER_VERBOSITY === 0 | ||
&& $sniffer->config->interactive === false | ||
&& $sniffer->config->showProgress === true | ||
) { | ||
echo PHP_EOL . PHP_EOL; | ||
} | ||
|
||
$ignoreWarnings = Config::getConfigData('ignore_warnings_on_exit'); | ||
$ignoreErrors = Config::getConfigData('ignore_errors_on_exit'); | ||
|
||
$return = ($sniffer->reporter->totalErrors + $sniffer->reporter->totalWarnings); | ||
if ($ignoreErrors !== null) { | ||
$ignoreErrors = (bool) $ignoreErrors; | ||
if ($ignoreErrors === true) { | ||
$return -= $sniffer->reporter->totalErrors; | ||
} | ||
} | ||
|
||
if ($ignoreWarnings !== null) { | ||
$ignoreWarnings = (bool) $ignoreWarnings; | ||
if ($ignoreWarnings === true) { | ||
$return -= $sniffer->reporter->totalWarnings; | ||
} | ||
} | ||
|
||
return $return; | ||
} | ||
} |