Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
538
src/Framework/Design/Service/ConventionChecker.php
Normal file
538
src/Framework/Design/Service/ConventionChecker.php
Normal file
@@ -0,0 +1,538 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user