Files
Jonathan Miller 1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
fix: standardize file headers — REPO rename, SPDX case, missing fields
- Update REPO: from MokoStandards-API to moko-platform in 125 files
- Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files
- Fix SPDX-LICENSE-IDENTIFIER case in 2 template files
- Add missing REPO: field to 3 files

Authored-by: Moko Consulting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 17:01:17 -05:00

178 lines
7.4 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Templates.Scripts.Validate
* INGROUP: MokoStandards.Templates
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /templates/scripts/validate/dolibarr_module.php
* BRIEF: Validate a Dolibarr module repository against MokoStandards requirements
* NOTE: Deployed to bin/validate_module.php in governed Dolibarr module repos.
* Run: php bin/validate_module.php [--path DIR] [--verbose] [--json]
*/
declare(strict_types=1);
// Deployed to bin/validate_module.php in org repos — vendor/ is one level up (repo root)
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\CliFramework;
/**
* Validates a Dolibarr module repository against MokoStandards requirements.
*
* Checks performed:
* - Required directories (src/, src/core/modules/, langs/en_US/, img/)
* - Required files (README.md, CHANGELOG.md, LICENSE, composer.json)
* - Module descriptor exists and declares a valid version
* - SPDX-License-Identifier header present in all PHP source files
* - No tab characters in YAML/JSON config files
* - No Windows path separators in PHP source
*/
class ValidateDolibarrModule extends CliFramework
{
protected function configure(): void
{
$this->setDescription('Validate a Dolibarr module repository against MokoStandards requirements');
$this->addArgument('--path', 'Repository root to validate', '.');
}
protected function run(): int
{
$path = rtrim((string) $this->getArgument('--path'), '/\\');
$passed = 0;
$failed = 0;
if (!is_dir($path)) {
$this->error("Path does not exist: {$path}");
}
// ── Required directories ──────────────────────────────────────────
$this->section('Required directories');
foreach ([
'src',
'src/core/modules',
'langs/en_US',
'img',
] as $dir) {
$ok = is_dir("{$path}/{$dir}");
$this->status($ok, $dir . '/');
$ok ? $passed++ : $failed++;
}
// ── Required files ────────────────────────────────────────────────
$this->section('Required files');
foreach ([
'README.md',
'CHANGELOG.md',
'LICENSE',
'composer.json',
] as $file) {
$ok = file_exists("{$path}/{$file}");
$this->status($ok, $file);
$ok ? $passed++ : $failed++;
}
// ── Module descriptor ─────────────────────────────────────────────
$this->section('Module descriptor');
$descriptors = glob("{$path}/src/core/modules/mod*.class.php") ?: [];
if (empty($descriptors)) {
$this->status(false, 'mod*.class.php exists in src/core/modules/');
$failed++;
} else {
$this->status(true, 'mod*.class.php exists: ' . basename($descriptors[0]));
$passed++;
// Check $this->version is declared
$contents = (string) file_get_contents($descriptors[0]);
$hasVersion = (bool) preg_match('/\$this\s*->\s*version\s*=/', $contents);
$this->status($hasVersion, '$this->version declared in descriptor');
$hasVersion ? $passed++ : $failed++;
// Check module number is declared
$hasNumber = (bool) preg_match('/\$this\s*->\s*numero\s*=/', $contents);
$this->status($hasNumber, '$this->numero declared in descriptor');
$hasNumber ? $passed++ : $failed++;
}
// ── SPDX headers ──────────────────────────────────────────────────
$this->section('SPDX-License-Identifier headers');
$phpFiles = $this->findFiles($path . '/src', '*.php');
$missingHeader = 0;
foreach ($phpFiles as $file) {
$head = file_get_contents($file, false, null, 0, 512) ?: '';
if (!str_contains($head, 'SPDX-License-Identifier')) {
$missingHeader++;
$this->log('WARN', 'Missing SPDX header: ' . substr($file, strlen($path) + 1));
}
}
$ok = $missingHeader === 0;
$this->status($ok, "All PHP files have SPDX header ({$missingHeader} missing)");
$ok ? $passed++ : $failed++;
// ── No tabs in config files ───────────────────────────────────────
$this->section('No tab characters in config files');
$configFiles = array_merge(
$this->findFiles($path, '*.yml'),
$this->findFiles($path, '*.yaml'),
$this->findFiles($path, '*.json')
);
$tabFiles = 0;
foreach ($configFiles as $file) {
if (str_contains((string) file_get_contents($file), "\t")) {
$tabFiles++;
$this->log('WARN', 'Tab found: ' . substr($file, strlen($path) + 1));
}
}
$ok = $tabFiles === 0;
$this->status($ok, "No tabs in YAML/JSON ({$tabFiles} files affected)");
$ok ? $passed++ : $failed++;
// ── No backslash paths in PHP ─────────────────────────────────────
$this->section('No Windows path separators in PHP source');
$phpAll = $this->findFiles($path . '/src', '*.php');
$bsFiles = 0;
foreach ($phpAll as $file) {
if (preg_match('/["\'](?:[A-Za-z]:)?\\\\[^\\\\]/', (string) file_get_contents($file))) {
$bsFiles++;
$this->log('WARN', 'Backslash path: ' . substr($file, strlen($path) + 1));
}
}
$ok = $bsFiles === 0;
$this->status($ok, "No backslash paths ({$bsFiles} files affected)");
$ok ? $passed++ : $failed++;
// ── Summary ───────────────────────────────────────────────────────
$this->printSummary($passed, $failed, $this->elapsed());
return $failed > 0 ? 1 : 0;
}
/** Recursively find files matching a glob pattern under a directory. */
private function findFiles(string $dir, string $pattern): array
{
if (!is_dir($dir)) {
return [];
}
$results = [];
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iter as $file) {
if ($file->isFile() && fnmatch($pattern, $file->getFilename())) {
$results[] = $file->getPathname();
}
}
return $results;
}
}
$script = new ValidateDolibarrModule('validate_module', 'Validate a Dolibarr module repository against MokoStandards requirements');
exit($script->execute());