Resolve all non-false-positive PHPStan 2 findings, reducing the baseline from 486 to 382 entries (only unused-public false positives remain). Categories fixed: - Remove 39 redundant type checks (is_string, is_array, is_object, method_exists) - Remove 12 no-op array_values() on already-sequential lists - Simplify 13 null-coalesce on guaranteed offsets - Remove 8 always-true instanceof checks - Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount) - Simplify 6 unnecessary nullsafe operators (?-> → ->) - Fix 6 always-true !== '' comparisons - Fix remaining: unset.offset, return.unusedType, staticMethod narrowing 53 files changed across core/, modules/, pages/, and tests/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.7 KiB
PHP
142 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Architecture;
|
|
|
|
final class ModuleManifestContractTest extends AbstractModuleStructureContractTestCase
|
|
{
|
|
public function testAtLeastOneModuleExists(): void
|
|
{
|
|
self::assertNotEmpty(self::$modules, 'Expected at least one module under modules/');
|
|
}
|
|
|
|
public function testEveryModuleHasManifest(): void
|
|
{
|
|
$modulesDir = dirname(__DIR__, 2) . '/modules';
|
|
if (!is_dir($modulesDir)) {
|
|
self::markTestSkipped('No modules/ directory');
|
|
}
|
|
|
|
$entries = scandir($modulesDir) ?: [];
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) {
|
|
continue;
|
|
}
|
|
self::assertFileExists(
|
|
$modulesDir . '/' . $entry . '/module.php',
|
|
"Module directory '{$entry}' is missing module.php manifest"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testManifestIdMatchesDirectoryName(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
self::assertSame(
|
|
$module['id'],
|
|
$module['manifest']->id,
|
|
"Module directory '{$module['id']}' has mismatched manifest id '{$module['manifest']->id}'"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testManifestHasVersion(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
self::assertMatchesRegularExpression(
|
|
'/^\d+\.\d+\.\d+$/',
|
|
$module['manifest']->version,
|
|
"Module '{$module['id']}' must have a semver version (got '{$module['manifest']->version}')"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testManifestHasAllRequiredKeys(): void
|
|
{
|
|
$requiredKeys = [
|
|
'id', 'version', 'enabled_by_default', 'load_order',
|
|
'routes', 'public_paths', 'container_registrars',
|
|
'ui_slots', 'search_resources', 'asset_groups',
|
|
'scheduler_jobs', 'layout_context_providers',
|
|
'session_providers', 'permissions',
|
|
];
|
|
|
|
foreach (self::$modules as $module) {
|
|
foreach ($requiredKeys as $key) {
|
|
self::assertArrayHasKey(
|
|
$key,
|
|
$module['raw'],
|
|
"Module '{$module['id']}' manifest is missing required key '{$key}'"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testRequiresFieldContainsOnlyValidModuleIds(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
foreach ($module['manifest']->requires as $index => $requiredId) {
|
|
self::assertNotEmpty(
|
|
trim($requiredId),
|
|
"Module '{$module['id']}' requires[{$index}] must be a non-empty string"
|
|
);
|
|
self::assertMatchesRegularExpression(
|
|
'/^[a-z][a-z0-9_-]*$/',
|
|
$requiredId,
|
|
"Module '{$module['id']}' requires[{$index}] has invalid module id format: '{$requiredId}'"
|
|
);
|
|
}
|
|
}
|
|
|
|
self::addToAssertionCount(1);
|
|
}
|
|
|
|
public function testModuleHasLibDirectory(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
self::assertDirectoryExists(
|
|
$module['path'] . '/lib',
|
|
"Module '{$module['id']}' must have a lib/ directory for PHP classes"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testRoutesHavePathAndTarget(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
foreach ($module['manifest']->routes as $idx => $route) {
|
|
self::assertArrayHasKey(
|
|
'path',
|
|
$route,
|
|
"Module '{$module['id']}' route[{$idx}] is missing 'path'"
|
|
);
|
|
self::assertArrayHasKey(
|
|
'target',
|
|
$route,
|
|
"Module '{$module['id']}' route[{$idx}] is missing 'target'"
|
|
);
|
|
self::assertNotEmpty(
|
|
trim((string) $route['path']),
|
|
"Module '{$module['id']}' route[{$idx}] has empty 'path'"
|
|
);
|
|
self::assertNotEmpty(
|
|
trim((string) $route['target']),
|
|
"Module '{$module['id']}' route[{$idx}] has empty 'target'"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testModulesWithRoutesHavePagesDirectory(): void
|
|
{
|
|
foreach (self::$modules as $module) {
|
|
if (empty($module['manifest']->routes)) {
|
|
continue;
|
|
}
|
|
self::assertDirectoryExists(
|
|
$module['path'] . '/pages',
|
|
"Module '{$module['id']}' declares routes but has no pages/ directory"
|
|
);
|
|
}
|
|
}
|
|
}
|