Skip to content
Open
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
42 changes: 40 additions & 2 deletions src/Documentation/DocumentationMenuItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@
final readonly class DocumentationMenuItem
{
/**
* @param non-empty-string|null $slug
* @param string $href The URL path for the documentation menu item. Must be a non-empty string starting with '/', 'http://', or 'https://'
* @param string $label The display text for the menu item. Must be a non-empty string
* @param non-empty-string|null $slug The unique identifier for the documentation file. If null, indicates a menu item without content
* @param bool $isNew Whether this menu item represents new documentation
*/
public function __construct(
private string $href,
private string $label,
private ?string $slug,
private bool $isNew = false,
) {
Assert::notEmpty($href, 'Documentation href cannot be empty');
Assert::true(
str_starts_with($href, '/') || str_starts_with($href, 'http://') || str_starts_with($href, 'https://'),
'Documentation href must start with a forward slash, http://, or https://'
);
Assert::notEmpty($label, 'Documentation label cannot be empty');
}

public function getHref(): string
Expand All @@ -43,12 +52,41 @@ public function getSlug(): ?string
return $this->slug;
}

/**
* Checks if the documentation file exists for this menu item.
* Throws an exception if the file does not exist when a slug is provided.
*
* @return bool True if the documentation file exists, false if slug is null
* @throws \Webmozart\Assert\InvalidArgumentException If the file does not exist when slug is provided
*/
public function hasDocumentation(): bool
{
if ($this->slug === null) {
return false;
}

$documentationFilePath = $this->getDocumentationFilePath();
Assert::fileExists($documentationFilePath, sprintf('Documentation file must exist at "%s"', $documentationFilePath));

return true;
}

public function getMarkdownContents(): string
{
Assert::notNull($this->slug);
$documentationFilePath = __DIR__ . '/../../resources/docs/' . $this->slug . '.md';
$documentationFilePath = $this->getDocumentationFilePath();

Assert::fileExists($documentationFilePath);
return FileSystem::read($documentationFilePath);
}

/**
* Gets the full path to the documentation file.
*
* @return string The absolute path to the documentation file
*/
private function getDocumentationFilePath(): string
{
return __DIR__ . '/../../resources/docs/' . $this->slug . '.md';
}
}
71 changes: 67 additions & 4 deletions src/ValueObject/AppliedRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@

use App\Exception\ShouldNotHappenException;
use Nette\Utils\Strings;
use Webmozart\Assert\Assert;

final readonly class AppliedRule
{
private const EXPECTED_CLASS_PARTS_COUNT_MIN = 5;
private const EXPECTED_CLASS_PARTS_COUNT_MAX = 6;
private const CATEGORY_INDEX = 1;
private const RECTOR_LITERAL_INDEX_5_PARTS = 2;
private const RECTOR_LITERAL_INDEX_6_PARTS = 3;
private const NODE_CLASS_INDEX_5_PARTS = 3;
private const NODE_CLASS_INDEX_6_PARTS = 4;
private const SHORT_CLASS_INDEX_5_PARTS = 4;
private const SHORT_CLASS_INDEX_6_PARTS = 5;

private string $shortRectorClass;

public function __construct(
Expand Down Expand Up @@ -40,16 +51,68 @@ public function getTestFixtureNamespace(): string
public function getTestFixtureDirectoryPath(): string
{
$classParts = explode('\\', $this->rectorClass);
$partsCount = count($classParts);

// Validate class structure - allow 5 or 6 parts (6 parts when version number is present)
Assert::true(
$partsCount >= self::EXPECTED_CLASS_PARTS_COUNT_MIN && $partsCount <= self::EXPECTED_CLASS_PARTS_COUNT_MAX,
sprintf(
'Rector class "%s" must have %d or %d parts (e.g. "Rector\\Category\\Rector\\Node\\SomeRector" or "Rector\\Category\\Version\\Rector\\Node\\SomeRector")',
$this->rectorClass,
self::EXPECTED_CLASS_PARTS_COUNT_MIN,
self::EXPECTED_CLASS_PARTS_COUNT_MAX
)
);

$category = $classParts[1];
$rulesDirectory = 'rules-tests/' . $category;
// Validate first part is "Rector"
Assert::same($classParts[0], 'Rector', sprintf('Rector class "%s" must start with "Rector"', $this->rectorClass));

// Determine if we have a version part (6 parts) or not (5 parts)
$hasVersion = $partsCount === self::EXPECTED_CLASS_PARTS_COUNT_MAX;

// Validate "Rector" literal is at the correct position
$rectorLiteralIndex = $hasVersion ? self::RECTOR_LITERAL_INDEX_6_PARTS : self::RECTOR_LITERAL_INDEX_5_PARTS;
Assert::same(
$classParts[$rectorLiteralIndex],
'Rector',
sprintf('Rector class "%s" must have "Rector" at index %d', $this->rectorClass, $rectorLiteralIndex)
);

$nodeClass = $classParts[3];
$shortClass = $classParts[4];
// Get required parts with correct indices
// When there's a version (6 parts), use the version as the category; otherwise use the base category
$categoryIndex = $hasVersion ? 2 : self::CATEGORY_INDEX;
$category = $this->getClassPart($classParts, $categoryIndex, 'category');
$nodeClassIndex = $hasVersion ? self::NODE_CLASS_INDEX_6_PARTS : self::NODE_CLASS_INDEX_5_PARTS;
$shortClassIndex = $hasVersion ? self::SHORT_CLASS_INDEX_6_PARTS : self::SHORT_CLASS_INDEX_5_PARTS;

$nodeClass = $this->getClassPart($classParts, $nodeClassIndex, 'node class');
$shortClass = $this->getClassPart($classParts, $shortClassIndex, 'short class');

$rulesDirectory = 'rules-tests/' . $category;

return $rulesDirectory . '/Rector/' . $nodeClass . '/' . $shortClass . '/Fixture';
}

/**
* @param string[] $classParts
*/
private function getClassPart(array $classParts, int $index, string $partName): string
{
Assert::keyExists(
$classParts,
$index,
sprintf('Missing %s in rector class "%s"', $partName, $this->rectorClass)
);

$part = $classParts[$index];
Assert::notEmpty(
$part,
sprintf('Empty %s in rector class "%s"', $partName, $this->rectorClass)
);

return $part;
}

/**
* Mimics @see \App\RuleFilter\ValueObject\RuleMetadata::getSlug()
*/
Expand Down
Loading