Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ A package MUST use these names for these root-level files:
A package SHOULD include a root-level file indicating the licensing and
copyright terms of the package contents.

## Validator

Quickly validate your project's compliance by following these steps:

- Install package in your project: `composer require pds/skeleton @dev`
- Run the validator: `./vendor/pds/skeleton/bin/validate`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the validator be exposed by composer in vendor/bin ? This would be more common (it should be renamed to a less generic name to avoid clashes of course)


## Root-Level Directories

### bin/
Expand Down
291 changes: 291 additions & 0 deletions bin/validate
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
#!/usr/bin/env php

<?php

if (!defined('ENV') || ENV != 'test') {
$lines = scandir(__DIR__ . "/../../../../");
foreach ($lines as $i => $line) {
if (is_dir($line)) {
$lines[$i] .= "/";
}
}
$validator = new ComplianceValidator();
$results = $validator->validate($lines);
$validator->outputResults($results);
}

class ComplianceValidator
{
const STATE_OPTIONAL_NOT_PRESENT = 1;
const STATE_CORRECT_PRESENT = 2;
const STATE_REQUIRED_NOT_PRESENT = 3;
const STATE_INCORRECT_PRESENT = 4;

public function validate($lines)
{
$complianceTests = [
"Command-line executables" => $this->checkBin($lines),
"Configuration files" => $this->checkConfig($lines),
"Documentation files" => $this->checkDocs($lines),
"Web server files" => $this->checkPublic($lines),
"Other resource files" => $this->checkResources($lines),
"PHP source code" => $this->checkSrc($lines),
"Test code" => $this->checkTests($lines),
"Package managers" => $this->checkVendor($lines),
"Log of changes between releases" => $this->checkChangelog($lines),
"Guidelines for contributors" => $this->checkContributing($lines),
"Licensing information" => $this->checkLicense($lines),
"Information about the package itself" => $this->checkReadme($lines),
];

$results = [];
foreach ($complianceTests as $label => $complianceResult) {
$state = $complianceResult[0];
$expected = $complianceResult[1];
$actual = $complianceResult[2];
$results[$expected] = [
'label' => $label,
'state' => $state,
'expected' => $expected,
'actual' => $actual,
];
}
return $results;
}

public function outputResults($results)
{
foreach ($results as $result) {
$this->outputResultLine($result['label'], $result['state'], $result['expected'], $result['actual']);
}
}

protected function outputResultLine($label, $complianceState, $expected, $actual)
{
$messages = [
self::STATE_OPTIONAL_NOT_PRESENT => "Optional {$expected} not present",
self::STATE_CORRECT_PRESENT => "Correct {$actual} present",
self::STATE_INCORRECT_PRESENT => "Incorrect {$actual} present",
self::STATE_REQUIRED_NOT_PRESENT => "Required {$expected} not present",
];
echo $this->colorConsoleText("- " . $label . ": " . $messages[$complianceState], $complianceState) . PHP_EOL;
}

protected function colorConsoleText($text, $complianceState)
{
$colors = [
self::STATE_OPTIONAL_NOT_PRESENT => "\033[43;30m",
self::STATE_CORRECT_PRESENT => "\033[42;30m",
self::STATE_INCORRECT_PRESENT => "\033[41m",
self::STATE_REQUIRED_NOT_PRESENT => "\033[41m",
];
if (!array_key_exists($complianceState, $colors)) {
return $text;
}
return $colors[$complianceState] . " " . $text . " \033[0m";
}

protected function checkDir($lines, $pass, array $fail)
{
foreach ($lines as $line) {
$line = trim($line);
if ($line == $pass) {
return [self::STATE_CORRECT_PRESENT, $pass, $line];
}
if (in_array($line, $fail)) {
return [self::STATE_INCORRECT_PRESENT, $pass, $line];
}
}
return [self::STATE_OPTIONAL_NOT_PRESENT, $pass, null];
}

protected function checkFile($lines, $pass, array $fail)
{
foreach ($lines as $line) {
$line = trim($line);
if (preg_match("/^{$pass}(\.[a-z]+)?$/", $line)) {
return [self::STATE_CORRECT_PRESENT, $pass, $line];
}
foreach ($fail as $regex) {
if (preg_match($regex, $line)) {
return [self::STATE_INCORRECT_PRESENT, $pass, $line];
}
}
}
return [self::STATE_OPTIONAL_NOT_PRESENT, $pass, null];
}

protected function checkVendor($lines, $pass = 'vendor/')
{
foreach ($lines as $line) {
$line = trim($line);
if ($line == $pass) {
return [self::STATE_CORRECT_PRESENT, $pass, $line];
}
}
return [self::STATE_REQUIRED_NOT_PRESENT, $pass, null];
}

protected function checkChangelog($lines)
{
return $this->checkFile($lines, 'CHANGELOG', [
'/^.*CHANGLOG.*$/i',
'/^.*CAHNGELOG.*$/i',
'/^WHATSNEW(\.[a-z]+)?$/i',
'/^RELEASE((_|-)?NOTES)?(\.[a-z]+)?$/i',
'/^RELEASES(\.[a-z]+)?$/i',
'/^CHANGES(\.[a-z]+)?$/i',
'/^CHANGE(\.[a-z]+)?$/i',
'/^HISTORY(\.[a-z]+)?$/i',
]);
}

protected function checkContributing($lines)
{
return $this->checkFile($lines, 'CONTRIBUTING', [
'/^DEVELOPMENT(\.[a-z]+)?$/i',
'/^README\.CONTRIBUTING(\.[a-z]+)?$/i',
'/^DEVELOPMENT_README(\.[a-z]+)?$/i',
'/^CONTRIBUTE(\.[a-z]+)?$/i',
'/^HACKING(\.[a-z]+)?$/i',
]);
}

protected function checkLicense($lines)
{
return $this->checkFile($lines, 'LICENSE', [
'/^.*EULA.*$/i',
'/^.*(GPL|BSD).*$/i',
'/^([A-Z-]+)?LI(N)?(S|C)(E|A)N(S|C)(E|A)(_[A-Z_]+)?(\.[a-z]+)?$/i',
'/^COPY(I)?NG(\.[a-z]+)?$/i',
'/^COPYRIGHT(\.[a-z]+)?$/i',
]);
}

protected function checkReadme($lines)
{
return $this->checkFile($lines, 'README', [
'/^USAGE(\.[a-z]+)?$/i',
'/^SUMMARY(\.[a-z]+)?$/i',
'/^DESCRIPTION(\.[a-z]+)?$/i',
'/^IMPORTANT(\.[a-z]+)?$/i',
'/^NOTICE(\.[a-z]+)?$/i',
'/^GETTING(_|-)STARTED(\.[a-z]+)?$/i',
]);
}

protected function checkBin($lines)
{
return $this->checkDir($lines, 'bin/', [
'cli/',
'scripts/',
'console/',
'shell/',
'script/',
]);
}

protected function checkConfig($lines)
{
return $this->checkDir($lines, 'config/', [
'etc/',
'settings/',
'configuration/',
'configs/',
'_config/',
'conf/',
]);
}

protected function checkDocs($lines)
{
return $this->checkDir($lines, 'docs/', [
'manual/',
'documentation/',
'usage/',
'doc/',
'guide/',
'phpdoc/',
]);
}

protected function checkPublic($lines)
{
return $this->checkDir($lines, 'public/', [
'assets/',
'static/',
'html/',
'httpdocs/',
'media/',
'docroot/',
'css/',
'fonts/',
'styles/',
'style/',
'js/',
'javascript/',
'images/',
'site/',
'mysite/',
'img/',
'web/',
'pub/',
'webroot/',
'www/',
'htdocs/',
'asset/',
'public_html/',
'publish/',
'pages/',
]);
}

protected function checkSrc($lines)
{
return $this->checkDir($lines, 'src/', [
'exception/',
'exceptions/',
'src-files/',
'traits/',
'interfaces/',
'common/',
'sources/',
'php/',
'inc/',
'libraries/',
'autoloads/',
'autoload/',
'source/',
'includes/',
'include/',
'lib/',
'libs/',
'library/',
'code/',
'classes/',
'func/',
]);
}

protected function checkTests($lines)
{
return $this->checkDir($lines, 'tests/', [
'test/',
'unit-tests/',
'phpunit/',
'testing/',
]);
}

protected function checkResources($lines)
{
return $this->checkDir($lines, 'resources/', [
'Resources/',
'res/',
'resource/',
'Resource/',
'ressources/',
'Ressources/',
]);
}
}
69 changes: 69 additions & 0 deletions tests/ComplianceValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

define('ENV', 'test');
require __DIR__ . "/../bin/validate";

$tester = new ComplianceValidatorTest();
// Test all 4 possible states.
$tester->testValidate_WithIncorrectBin_ReturnsIncorrectBin();
$tester->testValidate_WithoutVendor_ReturnsMissingVendor();

echo "Errors: {$tester->numErrors}" . PHP_EOL;

class ComplianceValidatorTest
{
public $numErrors = 0;

public function testValidate_WithIncorrectBin_ReturnsIncorrectBin()
{
$paths = [
'cli/',
'vendor/',
];

$validator = new ComplianceValidator();
$results = $validator->validate($paths);

foreach ($results as $expected => $result) {
if ($expected == "bin/") {
if ($result['state'] != ComplianceValidator::STATE_INCORRECT_PRESENT) {
$this->numErrors++;
echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_INCORRECT_PRESENT" . PHP_EOL;
}
continue;
}
if ($expected == "vendor/") {
if ($result['state'] != ComplianceValidator::STATE_CORRECT_PRESENT) {
$this->numErrors++;
echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_CORRECT_PRESENT" . PHP_EOL;
}
continue;
}
if ($result['state'] != ComplianceValidator::STATE_OPTIONAL_NOT_PRESENT) {
$this->numErrors++;
echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_OPTIONAL_NOT_PRESENT" . PHP_EOL;
continue;
}
}
}

public function testValidate_WithoutVendor_ReturnsMissingVendor()
{
$paths = [
'bin/',
];

$validator = new ComplianceValidator();
$results = $validator->validate($paths);

foreach ($results as $expected => $result) {
if ($expected == "vendor/") {
if ($result['state'] != ComplianceValidator::STATE_REQUIRED_NOT_PRESENT) {
$this->numErrors++;
echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_REQUIRED_NOT_PRESENT" . PHP_EOL;
}
continue;
}
}
}
}