/**
* Explain Rule Tool
* Provides detailed explanations for Magento coding standard rules.
*/
import {
INSECURE_FUNCTIONS,
DISCOURAGED_FUNCTIONS,
RESTRICTED_CLASSES,
XSS_ESCAPE_METHODS,
MAGENTO_RULES,
getSeverityInfo,
} from '../knowledge/index.js';
import { getActiveTheme } from '../themes/index.js';
export interface RuleExplanation {
rule: string;
category: string;
severity: number;
severityLabel: string;
type: 'error' | 'warning';
description: string;
reasoning: string;
badExample?: string;
goodExample?: string;
relatedRules?: string[];
documentation?: string;
}
/**
* Detailed rule explanations
*/
const RULE_EXPLANATIONS: Record<string, Omit<RuleExplanation, 'rule' | 'severity' | 'severityLabel' | 'type'>> = {
'Magento2.Security.InsecureFunction': {
category: 'Security',
description: 'Detects use of insecure functions that pose security risks.',
reasoning: `These functions are forbidden because they can lead to:
- Command injection (exec, shell_exec, system, passthru, popen, proc_open)
- Code injection (eval, assert, create_function)
- Object injection (serialize, unserialize)
- Weak cryptography (md5, mt_rand, srand)
Magento provides secure alternatives that should be used instead.`,
badExample: `<?php
// DANGEROUS - Command injection risk
$output = shell_exec('ls ' . $userInput);
// DANGEROUS - Object injection risk
$data = unserialize($userString);
// DANGEROUS - Weak hash
$hash = md5($password);`,
goodExample: `<?php
// Use Magento Shell class
$output = $this->shell->execute('ls %s', [$escapedInput]);
// Use JSON serialization
$data = $this->jsonSerializer->unserialize($userString);
// Use secure hashing
$hash = password_hash($password, PASSWORD_DEFAULT);`,
relatedRules: ['Squiz.PHP.Eval'],
documentation: 'https://devdocs.magento.com/guides/v2.4/extension-dev-guide/security/'
},
'Magento2.Security.XssTemplate': {
category: 'Security',
description: 'Detects unescaped output in PHTML templates that could lead to XSS vulnerabilities.',
reasoning: `Cross-Site Scripting (XSS) attacks occur when untrusted data is rendered without proper escaping.
All dynamic output in templates MUST be escaped using the appropriate escape method.
Available escape methods:
- escapeHtml(): For general HTML content
- escapeHtmlAttr(): For HTML attribute values
- escapeUrl(): For URLs in href/src
- escapeJs(): For JavaScript string values
- escapeCss(): For CSS values`,
badExample: `<?php // BAD - XSS vulnerability ?>
<?= $block->getUserInput() ?>
<div data-value="<?= $block->getValue() ?>">
<a href="<?= $block->getUrl() ?>">Link</a>`,
goodExample: `<?php // GOOD - Properly escaped ?>
<?= $escaper->escapeHtml($block->getUserInput()) ?>
<div data-value="<?= $escaper->escapeHtmlAttr($block->getValue()) ?>">
<a href="<?= $escaper->escapeUrl($block->getUrl()) ?>">Link</a>
<?php // OK to skip escape for already-safe output ?>
<?= /* @noEscape */ $block->getJsLayout() ?>`,
relatedRules: ['Magento2.Templates.ThisInTemplate'],
documentation: 'https://devdocs.magento.com/guides/v2.4/extension-dev-guide/xss-protection.html'
},
'Magento2.Templates.ThisInTemplate': {
category: 'Templates',
description: 'Warns about deprecated $this usage in templates and discouraged helper usage.',
reasoning: `In Magento 2 templates:
- $this is deprecated, use $block instead for better clarity
- Helpers in templates are discouraged, use ViewModels instead
ViewModels provide:
- Better testability (can be unit tested)
- Cleaner separation of concerns
- Proper dependency injection`,
badExample: `<?php // DEPRECATED ?>
<?= $this->getTitle() ?>
<?= $this->helper('Vendor\\Module\\Helper\\Data')->getValue() ?>`,
goodExample: `<?php
/** @var \\Magento\\Framework\\View\\Element\\Template $block */
/** @var \\Vendor\\Module\\ViewModel\\MyViewModel $viewModel */
$viewModel = $block->getData('viewModel');
?>
<?= $escaper->escapeHtml($block->getTitle()) ?>
<?= $escaper->escapeHtml($viewModel->getValue()) ?>`,
relatedRules: ['Magento2.Templates.ObjectManager'],
},
'Magento2.Functions.DiscouragedFunction': {
category: 'Functions',
description: 'Detects usage of discouraged PHP functions that have Magento alternatives.',
reasoning: `Magento provides wrapper classes for many PHP functions to ensure:
- Consistent behavior across different server configurations
- Compatibility with cloud/remote storage
- Better testability through dependency injection
- Proper abstraction for the Magento ecosystem`,
badExample: `<?php
// Discouraged - not compatible with remote storage
$content = file_get_contents($path);
$exists = file_exists($path);
mkdir($dir, 0755);`,
goodExample: `<?php
// Use Magento Filesystem
$content = $this->driver->fileGetContents($path);
$exists = $this->driver->isExists($path);
$this->driver->createDirectory($dir, 0755);`,
relatedRules: ['Magento2.Security.InsecureFunction'],
},
'Magento2.Legacy.RestrictedCode': {
category: 'Legacy',
description: 'Detects usage of deprecated Zend Framework classes that should be replaced.',
reasoning: `Zend Framework 1 classes are deprecated and should be replaced with:
- Laminas equivalents (Zend Framework successor)
- Native Magento Framework classes
This ensures long-term compatibility and support.`,
badExample: `<?php
use Zend_Json;
use Zend_Validate_EmailAddress;
$json = Zend_Json::encode($data);
$validator = new Zend_Validate_EmailAddress();`,
goodExample: `<?php
use Magento\\Framework\\Serialize\\Serializer\\Json;
use Magento\\Framework\\Validator\\EmailAddress;
$json = $this->jsonSerializer->serialize($data);
$validator = new EmailAddress();`,
relatedRules: ['Magento2.Legacy.MageEntity'],
},
'Magento2.PHP.FinalImplementation': {
category: 'PHP',
description: 'Prohibits the use of "final" keyword on classes and methods.',
reasoning: `The "final" keyword is forbidden in Magento because:
- It prevents class extension, breaking Magento's extensibility model
- It's incompatible with Magento's plugin (interceptor) system
- It's incompatible with proxy classes for lazy loading
- It limits customization options for merchants and developers`,
badExample: `<?php
final class MyClass // ERROR
{
final public function myMethod() // ERROR
{
}
}`,
goodExample: `<?php
class MyClass // OK - can be extended and intercepted
{
public function myMethod() // OK - plugins can intercept
{
}
}`,
relatedRules: [],
},
'Magento2.Classes.DiscouragedDependencies': {
category: 'Classes',
description: 'Prevents explicit proxy/interceptor injection and ObjectManager usage.',
reasoning: `Magento's DI container automatically creates:
- Proxies for lazy loading
- Interceptors for plugins
Explicitly requesting these classes:
- Bypasses the automatic generation
- Creates tight coupling
- May cause unexpected behavior`,
badExample: `<?php
// WRONG - explicit proxy
public function __construct(
\\Magento\\Catalog\\Model\\Product\\Proxy $productProxy
) {}
// WRONG - ObjectManager usage
$product = \\Magento\\Framework\\App\\ObjectManager::getInstance()
->get(ProductInterface::class);`,
goodExample: `<?php
// CORRECT - let DI handle proxies
public function __construct(
ProductInterface $product
) {}
// Configure proxy in di.xml if needed:
// <type name="MyClass">
// <arguments>
// <argument name="product" xsi:type="object">
// Magento\\Catalog\\Api\\Data\\ProductInterface\\Proxy
// </argument>
// </arguments>
// </type>`,
relatedRules: ['Magento2.Templates.ObjectManager'],
},
'Magento2.SQL.RawQuery': {
category: 'SQL',
description: 'Detects raw SQL queries that should use repositories or query builders.',
reasoning: `Raw SQL queries are discouraged because:
- SQL injection risk if not properly parameterized
- Database portability issues
- Bypasses Magento's entity events and plugins
- Harder to maintain and test
Use repositories, collections, or query builder instead.`,
badExample: `<?php
// DANGEROUS - SQL injection risk
$query = "SELECT * FROM catalog_product WHERE sku = '" . $sku . "'";
$result = $connection->query($query);`,
goodExample: `<?php
// Use repository
$product = $this->productRepository->get($sku);
// Or collection
$collection = $this->productCollectionFactory->create();
$collection->addFieldToFilter('sku', $sku);
// Or query builder with binding
$select = $connection->select()
->from('catalog_product')
->where('sku = ?', $sku);
$result = $connection->fetchAll($select);`,
relatedRules: ['Magento2.Security.Superglobal'],
},
'Magento2.Translation.ConstantUsage': {
category: 'Translation',
description: 'Forbids using constants as the first argument of __() or Phrase().',
reasoning: `Translation strings must be string literals because:
- Translation tools scan for __('string') patterns
- Constants cannot be detected by static analysis
- Translations may not be collected properly`,
badExample: `<?php
const MY_MESSAGE = 'Hello World';
echo __(self::MY_MESSAGE); // ERROR
$message = 'Hello';
echo __($message); // ERROR - variable`,
goodExample: `<?php
echo __('Hello World'); // OK - string literal
echo __('Hello %1', $name); // OK - literal with placeholder`,
relatedRules: [],
},
};
/**
* Get explanation for a rule
*/
export function explainRule(ruleName: string): RuleExplanation | null {
// Check active theme rules first (e.g., Hyva.JS.NoJQuery)
const theme = getActiveTheme();
if (theme) {
const themeRule = theme.validationRules.find(r =>
r.rule.toLowerCase() === ruleName.toLowerCase() ||
r.rule.toLowerCase().includes(ruleName.toLowerCase())
);
if (themeRule) {
return {
rule: themeRule.rule,
category: theme.name,
severity: themeRule.severity,
severityLabel: themeRule.severity >= 9 ? 'Critical' : themeRule.severity >= 7 ? 'Warning' : 'Style',
type: themeRule.type,
description: themeRule.message,
reasoning: `This rule is part of ${theme.name} coding standards. ${themeRule.suggestion || ''}`,
badExample: `Code matching pattern: ${themeRule.pattern}`,
goodExample: themeRule.suggestion,
relatedRules: [],
};
}
}
// Find rule in MAGENTO_RULES
const rule = MAGENTO_RULES.find(r =>
r.name.toLowerCase() === ruleName.toLowerCase() ||
r.name.toLowerCase().includes(ruleName.toLowerCase())
);
if (!rule) {
return null;
}
const explanation = RULE_EXPLANATIONS[rule.name];
const severityInfo = getSeverityInfo(rule.severity);
if (explanation) {
return {
rule: rule.name,
severity: rule.severity,
severityLabel: severityInfo?.category || 'Unknown',
type: rule.type,
...explanation
};
}
// Return basic info if no detailed explanation
return {
rule: rule.name,
category: rule.category,
severity: rule.severity,
severityLabel: severityInfo?.category || 'Unknown',
type: rule.type,
description: rule.description,
reasoning: `This rule is part of Magento's ${rule.category} coding standards with severity ${rule.severity}.`
};
}
/**
* Search for rules by keyword
*/
export function searchRules(keyword: string): string[] {
const keywordLower = keyword.toLowerCase();
return MAGENTO_RULES
.filter(r =>
r.name.toLowerCase().includes(keywordLower) ||
r.description.toLowerCase().includes(keywordLower) ||
r.category.toLowerCase().includes(keywordLower)
)
.map(r => r.name);
}