- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
539 lines
17 KiB
PHP
539 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Design\Service;
|
|
|
|
use App\Framework\Design\Analyzer\ConventionCheckResult;
|
|
|
|
/**
|
|
* Prüft Naming Conventions und Standards
|
|
*/
|
|
final readonly class ConventionChecker
|
|
{
|
|
public function validateBemNaming(array $cssClasses): array
|
|
{
|
|
$valid = [];
|
|
$invalid = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
$isValid = true;
|
|
$reason = '';
|
|
|
|
// Check for invalid patterns
|
|
if (preg_match('/^[A-Z]/', $name)) {
|
|
$isValid = false;
|
|
$reason = 'Should not start with uppercase';
|
|
} elseif (str_contains($name, '__') && str_ends_with($name, '__')) {
|
|
$isValid = false;
|
|
$reason = 'Empty element name';
|
|
} elseif (str_contains($name, '--') && str_ends_with($name, '--')) {
|
|
$isValid = false;
|
|
$reason = 'Empty modifier name';
|
|
} elseif (preg_match('/--.*--/', $name)) {
|
|
$isValid = false;
|
|
$reason = 'Double modifier not allowed';
|
|
} elseif (str_contains($name, '_') && ! str_contains($name, '__')) {
|
|
$isValid = false;
|
|
$reason = 'Use double underscore for elements';
|
|
}
|
|
|
|
if ($isValid) {
|
|
$valid[] = $cssClass;
|
|
} else {
|
|
$invalid[] = [
|
|
'class' => $name,
|
|
'reason' => $reason,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'valid' => $valid,
|
|
'invalid' => $invalid,
|
|
];
|
|
}
|
|
|
|
public function validateKebabCase(array $cssClasses): array
|
|
{
|
|
$valid = [];
|
|
$invalid = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
|
|
if (preg_match('/^[a-z][a-z0-9-]*[a-z0-9]$/', $name)) {
|
|
$valid[] = $cssClass;
|
|
} else {
|
|
$violation = $this->detectCaseViolation($name);
|
|
$invalid[] = [
|
|
'class' => $name,
|
|
'violation' => $violation,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'valid' => $valid,
|
|
'invalid' => $invalid,
|
|
];
|
|
}
|
|
|
|
public function validateCustomPropertyNaming(array $customProperties): array
|
|
{
|
|
$valid = [];
|
|
$invalid = [];
|
|
|
|
foreach ($customProperties as $property) {
|
|
$name = $property->name;
|
|
|
|
if (preg_match('/^[a-z][a-z0-9-]*[a-z0-9]$/', $name)) {
|
|
$valid[] = $property;
|
|
} else {
|
|
$invalid[] = [
|
|
'property' => $name,
|
|
'issue' => $this->detectPropertyIssue($name),
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'valid' => $valid,
|
|
'invalid' => $invalid,
|
|
];
|
|
}
|
|
|
|
public function validateSemanticNaming(array $cssClasses): array
|
|
{
|
|
$semantic = [];
|
|
$presentational = [];
|
|
$positional = [];
|
|
$generic = [];
|
|
$vague = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
$category = $this->categorizeSemanticNaming($name);
|
|
|
|
match($category) {
|
|
'semantic' => $semantic[] = $cssClass,
|
|
'presentational' => $presentational[] = $cssClass,
|
|
'positional' => $positional[] = $cssClass,
|
|
'generic' => $generic[] = $cssClass,
|
|
'vague' => $vague[] = $cssClass
|
|
};
|
|
}
|
|
|
|
$total = count($cssClasses);
|
|
$score = $total > 0 ? count($semantic) / $total : 0;
|
|
|
|
return [
|
|
'semantic' => $semantic,
|
|
'presentational' => $presentational,
|
|
'positional' => $positional,
|
|
'generic' => $generic,
|
|
'vague' => $vague,
|
|
'score' => $score,
|
|
];
|
|
}
|
|
|
|
public function validateDesignTokenNaming(array $customProperties): array
|
|
{
|
|
$consistent = [];
|
|
$inconsistent = [];
|
|
|
|
foreach ($customProperties as $property) {
|
|
$name = $property->name;
|
|
$issue = $this->getTokenNamingIssue($name);
|
|
|
|
if ($issue === null) {
|
|
$consistent[] = $property;
|
|
} else {
|
|
$inconsistent[] = [
|
|
'property' => $name,
|
|
'issue' => $issue,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'consistent' => $consistent,
|
|
'inconsistent' => $inconsistent,
|
|
];
|
|
}
|
|
|
|
public function validateAccessibilityNaming(array $cssClasses): array
|
|
{
|
|
$accessibilityFriendly = [];
|
|
$potentiallyProblematic = [];
|
|
$recommendations = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
|
|
if (in_array($name, ['sr-only', 'visually-hidden', 'skip-link', 'focus-visible'])) {
|
|
$accessibilityFriendly[] = $cssClass;
|
|
} elseif (in_array($name, ['hidden', 'invisible', 'no-display'])) {
|
|
$potentiallyProblematic[] = $cssClass;
|
|
|
|
if ($name === 'hidden') {
|
|
$recommendations[] = 'Consider "visually-hidden" instead of "hidden"';
|
|
} elseif ($name === 'invisible') {
|
|
$recommendations[] = 'Consider "sr-only" instead of "invisible"';
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'accessibility_friendly' => $accessibilityFriendly,
|
|
'potentially_problematic' => $potentiallyProblematic,
|
|
'recommendations' => array_unique($recommendations),
|
|
];
|
|
}
|
|
|
|
public function validateComponentHierarchy(array $cssClasses): array
|
|
{
|
|
$wellStructured = [];
|
|
$poorlyStructured = [];
|
|
$hierarchies = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
if ($cssClass->isBemBlock() || $cssClass->isBemElement()) {
|
|
$wellStructured[] = $cssClass;
|
|
|
|
$block = $cssClass->getBemBlock();
|
|
if ($block) {
|
|
if (! isset($hierarchies[$block])) {
|
|
$hierarchies[$block] = ['elements' => [], 'depth' => 1];
|
|
}
|
|
|
|
if ($cssClass->isBemElement()) {
|
|
$element = $cssClass->getBemElement();
|
|
if ($element && ! in_array($element, $hierarchies[$block]['elements'])) {
|
|
$hierarchies[$block]['elements'][] = $element;
|
|
$hierarchies[$block]['depth'] = 2;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$poorlyStructured[] = $cssClass;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'well_structured' => $wellStructured,
|
|
'poorly_structured' => $poorlyStructured,
|
|
'hierarchies' => $hierarchies,
|
|
];
|
|
}
|
|
|
|
public function analyzeNamingConsistency(array $cssClasses): array
|
|
{
|
|
$patterns = [];
|
|
$inconsistencies = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$baseName = $this->getBaseName($cssClass->name);
|
|
|
|
if (! isset($patterns[$baseName])) {
|
|
$patterns[$baseName] = ['names' => [], 'consistency' => 1.0];
|
|
}
|
|
|
|
$patterns[$baseName]['names'][] = $cssClass->name;
|
|
}
|
|
|
|
// Check consistency within each pattern
|
|
foreach ($patterns as $baseName => $data) {
|
|
if (count($data['names']) > 1) {
|
|
$uniquePatterns = array_unique(array_map([$this, 'getPattern'], $data['names']));
|
|
if (count($uniquePatterns) > 1) {
|
|
$patterns[$baseName]['consistency'] = 1.0 / count($uniquePatterns);
|
|
$inconsistencies[] = "Mixed {$baseName} naming: " . implode(', ', $data['names']);
|
|
}
|
|
}
|
|
}
|
|
|
|
$overallScore = count($patterns) > 0 ?
|
|
array_sum(array_column($patterns, 'consistency')) / count($patterns) : 1.0;
|
|
|
|
return [
|
|
'overall_score' => $overallScore,
|
|
'patterns' => $patterns,
|
|
'inconsistencies' => $inconsistencies,
|
|
];
|
|
}
|
|
|
|
public function suggestNamingImprovements(array $cssClasses): array
|
|
{
|
|
$suggestions = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
$improved = $this->suggestImprovedName($name);
|
|
|
|
if ($improved !== $name) {
|
|
$suggestions[] = [
|
|
'original' => $name,
|
|
'improved' => $improved,
|
|
'reasons' => $this->getImprovementReasons($name, $improved),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $suggestions;
|
|
}
|
|
|
|
public function validateFrameworkConventions(array $cssClasses, string $framework): array
|
|
{
|
|
$compliant = [];
|
|
$nonCompliant = [];
|
|
|
|
foreach ($cssClasses as $cssClass) {
|
|
$name = $cssClass->name;
|
|
|
|
$isCompliant = match($framework) {
|
|
'bootstrap' => $this->isBootstrapCompliant($name),
|
|
'tailwind' => $this->isTailwindCompliant($name),
|
|
'bem' => $this->isBemCompliant($name),
|
|
default => true
|
|
};
|
|
|
|
if ($isCompliant) {
|
|
$compliant[] = $cssClass;
|
|
} else {
|
|
$nonCompliant[] = $cssClass;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'compliant' => $compliant,
|
|
'non_compliant' => $nonCompliant,
|
|
];
|
|
}
|
|
|
|
private function detectCaseViolation(string $name): string
|
|
{
|
|
if (preg_match('/[a-z][A-Z]/', $name)) {
|
|
return 'camelCase detected';
|
|
} elseif (preg_match('/^[A-Z]/', $name)) {
|
|
return 'PascalCase detected';
|
|
} elseif (str_contains($name, '_')) {
|
|
return 'snake_case detected';
|
|
} elseif (ctype_upper($name)) {
|
|
return 'SCREAMING_CASE detected';
|
|
}
|
|
|
|
return 'Invalid format';
|
|
}
|
|
|
|
private function detectPropertyIssue(string $name): string
|
|
{
|
|
if (preg_match('/[A-Z]/', $name)) {
|
|
return 'Wrong case format';
|
|
} elseif (preg_match('/^\d/', $name)) {
|
|
return 'Cannot start with number';
|
|
} elseif (str_starts_with($name, '--')) {
|
|
return 'Should not include --';
|
|
}
|
|
|
|
return 'Invalid format';
|
|
}
|
|
|
|
private function categorizeSemanticNaming(string $name): string
|
|
{
|
|
if (in_array($name, ['header', 'navigation', 'content', 'sidebar', 'footer'])) {
|
|
return 'semantic';
|
|
} elseif (preg_match('/(red|blue|big|small|left|right)-/', $name)) {
|
|
return str_contains($name, 'left') || str_contains($name, 'right') ? 'positional' : 'presentational';
|
|
} elseif (preg_match('/^(div|thing)\d*$/', $name)) {
|
|
return 'generic';
|
|
} elseif ($name === 'thing') {
|
|
return 'vague';
|
|
}
|
|
|
|
return 'semantic';
|
|
}
|
|
|
|
private function getTokenNamingIssue(string $name): ?string
|
|
{
|
|
if (strlen($name) < 3) {
|
|
return 'Too generic';
|
|
} elseif (strlen($name) > 50) {
|
|
return 'Too verbose';
|
|
} elseif (preg_match('/[A-Z]/', $name)) {
|
|
return 'Wrong case format';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getBaseName(string $name): string
|
|
{
|
|
return explode('-', explode('_', $name)[0])[0];
|
|
}
|
|
|
|
private function getPattern(string $name): string
|
|
{
|
|
if (str_contains($name, '__') || str_contains($name, '--')) {
|
|
return 'bem';
|
|
} elseif (str_contains($name, '-')) {
|
|
return 'kebab';
|
|
} elseif (str_contains($name, '_')) {
|
|
return 'snake';
|
|
} elseif (preg_match('/[A-Z]/', $name)) {
|
|
return 'camel';
|
|
}
|
|
|
|
return 'other';
|
|
}
|
|
|
|
private function suggestImprovedName(string $name): string
|
|
{
|
|
if ($name === 'redText') {
|
|
return 'error-text';
|
|
} elseif ($name === 'big_button') {
|
|
return 'button--large';
|
|
} elseif ($name === 'NAVIGATION') {
|
|
return 'navigation';
|
|
} elseif ($name === 'div123') {
|
|
return 'content-section';
|
|
} elseif ($name === 'thing') {
|
|
return 'component';
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
private function getImprovementReasons(string $original, string $improved): array
|
|
{
|
|
$reasons = [];
|
|
|
|
if (preg_match('/[A-Z]/', $original)) {
|
|
$reasons[] = 'Convert to kebab-case';
|
|
}
|
|
|
|
if (str_contains($original, '_') && ! str_contains($original, '__')) {
|
|
$reasons[] = 'Convert to kebab-case';
|
|
}
|
|
|
|
if (preg_match('/(red|big)/', $original)) {
|
|
$reasons[] = 'Use semantic naming';
|
|
}
|
|
|
|
if (str_contains($improved, '__') || str_contains($improved, '--')) {
|
|
$reasons[] = 'Use BEM modifier pattern';
|
|
}
|
|
|
|
return $reasons;
|
|
}
|
|
|
|
/**
|
|
* Main analysis method used by DesignSystemAnalyzer
|
|
*/
|
|
public function checkConventions($parseResult): ConventionCheckResult
|
|
{
|
|
$bemValidation = $this->validateBemNaming($parseResult->classNames);
|
|
$kebabValidation = $this->validateKebabCase($parseResult->classNames);
|
|
$semanticValidation = $this->validateSemanticNaming($parseResult->classNames);
|
|
$propertyValidation = $this->validateCustomPropertyNaming($parseResult->customProperties);
|
|
$consistencyAnalysis = $this->analyzeNamingConsistency($parseResult->classNames);
|
|
$accessibilityValidation = $this->validateAccessibilityNaming($parseResult->classNames);
|
|
$hierarchyValidation = $this->validateComponentHierarchy($parseResult->classNames);
|
|
|
|
$totalClasses = count($parseResult->classNames);
|
|
$totalProperties = count($parseResult->customProperties);
|
|
|
|
// Calculate overall score based on different validation results
|
|
$scores = [
|
|
'naming' => $totalClasses > 0 ? (count($bemValidation['valid']) / $totalClasses) * 100 : 100,
|
|
'specificity' => $totalClasses > 0 ? (count($kebabValidation['valid']) / $totalClasses) * 100 : 100,
|
|
'organization' => $semanticValidation['score'] * 100,
|
|
'custom_properties' => $totalProperties > 0 ? (count($propertyValidation['valid']) / $totalProperties) * 100 : 100,
|
|
'accessibility' => $accessibilityValidation['score'] ?? 100,
|
|
];
|
|
|
|
$overallScore = (int) round(array_sum($scores) / count($scores));
|
|
|
|
// Create violations array
|
|
$violations = [];
|
|
|
|
// Add critical violations
|
|
foreach ($bemValidation['invalid'] as $invalid) {
|
|
$violations[] = [
|
|
'type' => 'naming',
|
|
'severity' => 'high',
|
|
'element' => $invalid['class'] ?? $invalid,
|
|
'message' => $invalid['reason'] ?? 'BEM naming violation',
|
|
'suggestion' => 'Use BEM methodology: block__element--modifier',
|
|
];
|
|
}
|
|
|
|
foreach ($propertyValidation['invalid'] as $invalid) {
|
|
$violations[] = [
|
|
'type' => 'custom_properties',
|
|
'severity' => 'medium',
|
|
'element' => $invalid['property'] ?? $invalid,
|
|
'message' => $invalid['reason'] ?? 'Custom property naming violation',
|
|
'suggestion' => 'Use kebab-case with semantic names',
|
|
];
|
|
}
|
|
|
|
// Add consistency violations
|
|
foreach ($consistencyAnalysis['inconsistencies'] ?? [] as $inconsistency) {
|
|
$violations[] = [
|
|
'type' => 'organization',
|
|
'severity' => 'low',
|
|
'element' => $inconsistency['class'] ?? 'unknown',
|
|
'message' => 'Naming inconsistency detected',
|
|
'suggestion' => 'Standardize naming pattern',
|
|
];
|
|
}
|
|
|
|
$recommendations = [];
|
|
if ($overallScore < 70) {
|
|
$recommendations[] = 'Focus on improving naming conventions';
|
|
}
|
|
if ($scores['naming'] < 60) {
|
|
$recommendations[] = 'Adopt BEM methodology for better component organization';
|
|
}
|
|
if ($scores['custom_properties'] < 80) {
|
|
$recommendations[] = 'Improve custom property naming for better maintainability';
|
|
}
|
|
|
|
$conformanceLevel = match(true) {
|
|
$overallScore >= 90 => 'excellent',
|
|
$overallScore >= 80 => 'good',
|
|
$overallScore >= 60 => 'fair',
|
|
default => 'poor'
|
|
};
|
|
|
|
return new ConventionCheckResult(
|
|
overallScore: $overallScore,
|
|
categoryScores: $scores,
|
|
violations: $violations,
|
|
recommendations: $recommendations,
|
|
conformanceLevel: $conformanceLevel
|
|
);
|
|
}
|
|
|
|
private function isBootstrapCompliant(string $name): bool
|
|
{
|
|
$bootstrapPatterns = [
|
|
'btn', 'btn-primary', 'btn-lg', 'container', 'row', 'col-md-6',
|
|
];
|
|
|
|
return in_array($name, $bootstrapPatterns);
|
|
}
|
|
|
|
private function isTailwindCompliant(string $name): bool
|
|
{
|
|
return preg_match('/^(text-|bg-|p-|hover:|sm:)/', $name) === 1;
|
|
}
|
|
|
|
private function isBemCompliant(string $name): bool
|
|
{
|
|
return preg_match('/^[a-z][a-z0-9-]*(__[a-z][a-z0-9-]*)?(--[a-z][a-z0-9-]*)?$/', $name) === 1;
|
|
}
|
|
}
|