diff --git a/.gitignore b/.gitignore index 376a286..a3fdb39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ -/composer.lock -/vendor/ -.idea \ No newline at end of file +# PhpStorm +.idea + +# composer +/vendor + +# PhpStan +.phpstan*.neon +phpstan*.neon +!.phpstan.dist.neon +!.phpstan.dist.*.neon diff --git a/.phpstan.dist.neon b/.phpstan.dist.neon new file mode 100644 index 0000000..5e8cbe0 --- /dev/null +++ b/.phpstan.dist.neon @@ -0,0 +1,8 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon +parameters: + scanFiles: + - stubs/mock.stub + paths: + - src + level: 10 diff --git a/composer.json b/composer.json index 9b9bf82..1dd342f 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,24 @@ { "name": "macopedia/phpstan-magento1", "description": "Extension for PHPStan to allow analysis of Magento 1 code.", - "type": "library", + "type": "phpstan-extension", + "license": "MIT", "require": { - "phpstan/phpstan": "^1.12.11 | ^2.0.2", "php": ">= 7.4" }, - "replace": { - "inviqa/phpstan-magento1": "0.1.5", - "vianetz/phpstan-magento1": "0.1.5" + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0" }, "autoload": { "psr-4": { "PHPStanMagento1\\": "src/" } }, + "replace": { + "inviqa/phpstan-magento1": "0.1.5", + "vianetz/phpstan-magento1": "0.1.5" + }, "scripts": { "test-quality": [ "phpstan analyse" @@ -23,5 +27,11 @@ "@test-quality" ] }, - "license": "MIT" + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..6691616 --- /dev/null +++ b/composer.lock @@ -0,0 +1,127 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "34c8dd3e682b7043ead1ab0658ec1005", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "6c98c7600fc717b2c78c11ef60040d5b1e359c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6c98c7600fc717b2c78c11ef60040d5b1e359c82", + "reference": "6c98c7600fc717b2c78c11ef60040d5b1e359c82", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-11-17T14:17:00+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" + }, + "time": "2024-10-26T16:04:33+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">= 7.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/extension-mage-autoload.neon b/extension-mage-autoload.neon deleted file mode 100644 index 910e8fb..0000000 --- a/extension-mage-autoload.neon +++ /dev/null @@ -1,56 +0,0 @@ -# This configuration file doesn't use static reflection for loading Mage class (so it executes code of Mage file). -# Make sure you DON'T have local.xml file in your project when using this config. Otherwise you'll get errors -# about connecting to db -# this config will also throw warnings "PHP Warning: Constant DS already defined in", this is an issue in phpstan -# https://github.com/phpstan/phpstan/issues/6744#event-6194525980 -parametersSchema: - magentoRootPath: string() -parameters: - magentoRootPath: %currentWorkingDirectory%/htdocs - excludePaths: - - */app/code/local/*/*/data/* - - */app/code/local/*/*/sql/* - bootstrapFiles: - - %magentoRootPath%/app/Mage.php - - phpstan-bootstrap-mage-autoload.php - - typeAliases: - Mage_Catalog_Model_Entity_Product_Collection: 'Mage_Catalog_Model_Resource_Product_Collection' - callback: 'callable' - earlyTerminatingMethodCalls: - Mage: - - throwException - -services: - - - class: PHPStanMagento1\Reflection\Varien\Object\MagicMethodsReflectionExtension - tags: - - phpstan.broker.methodsClassReflectionExtension - - - class: PHPStanMagento1\Type\Mage\CoreBlockAbstract\Helper - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\CoreModelLayout\Helper - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\CoreModelLayout\GetBlockSingleton - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\GetModel - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\GetResourceModel - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\GetSingleton - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - - class: PHPStanMagento1\Type\Mage\Helper - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/extension.neon b/extension.neon index 09c30b3..93f711f 100644 --- a/extension.neon +++ b/extension.neon @@ -1,51 +1,76 @@ parametersSchema: magentoRootPath: string() + enforceMagicMethodDocBlock: bool() + useLocalXml: bool() parameters: magentoRootPath: %currentWorkingDirectory%/htdocs - excludePaths: - - */app/code/local/*/*/data/* - - */app/code/local/*/*/sql/* + enforceMagicMethodDocBlock: false + useLocalXml: false bootstrapFiles: - - phpstan-bootstrap.php - scanFiles: - - %magentoRootPath%/app/Mage.php - typeAliases: - Mage_Catalog_Model_Entity_Product_Collection: 'Mage_Catalog_Model_Resource_Product_Collection' - callback: 'callable' - earlyTerminatingMethodCalls: - Mage: - - throwException + - %currentWorkingDirectory%/app/Mage.php services: + mageCoreConfig: + class: PHPStanMagento1\Config\MageCoreConfig + arguments: + useLocalXml: %useLocalXml% + + ## Dynamic Return Type Extension to return correct class from Mage::getModel() etc - - class: PHPStanMagento1\Reflection\Varien\Object\MagicMethodsReflectionExtension + class: PHPStanMagento1\Type\MageTypeExtension + arguments: + mageCoreConfig: @mageCoreConfig + className: Mage tags: - - phpstan.broker.methodsClassReflectionExtension + - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - class: PHPStanMagento1\Type\Mage\CoreModelLayout\Helper + class: PHPStanMagento1\Type\MageTypeExtension + arguments: + mageCoreConfig: @mageCoreConfig + className: Mage_Core_Block_Abstract tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStanMagento1\Type\Mage\CoreBlockAbstract\Helper + class: PHPStanMagento1\Type\MageTypeExtension + arguments: + mageCoreConfig: @mageCoreConfig + className: Mage_Core_Model_Abstract tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStanMagento1\Type\Mage\CoreModelLayout\GetBlockSingleton + class: PHPStanMagento1\Type\MageTypeExtension + arguments: + mageCoreConfig: @mageCoreConfig + className: Mage_Core_Model_Layout tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStanMagento1\Type\Mage\GetModel + class: PHPStanMagento1\Type\MageTypeExtension + arguments: + mageCoreConfig: @mageCoreConfig + className: Mage_Core_Controller_Varien_Action tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.broker.dynamicMethodReturnTypeExtension + + ## Rule to detect invalid class names returned by the Dynamic Return Type Extension - - class: PHPStanMagento1\Type\Mage\GetResourceModel + class: PHPStanMagento1\Rules\MageInvalidTypeRule + arguments: + mageCoreConfig: @mageCoreConfig tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.rules.rule + + ## Class Reflection Extension for Varien_Object's magic methods - - class: PHPStanMagento1\Type\Mage\GetSingleton + class: PHPStanMagento1\Reflection\VarienObjectReflectionExtension + arguments: + enforceDocBlock: %enforceMagicMethodDocBlock% tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.broker.methodsClassReflectionExtension + + ## PHP-Parser Extension to allow phtml and data install scripts to access protected methods with $this - - class: PHPStanMagento1\Type\Mage\Helper + class: PHPStanMagento1\PhpDoc\BindThisScopeResolverExtension tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.parser.richParserNodeVisitor + - phpstan.phpDoc.typeNodeResolverExtension diff --git a/phpstan-bootstrap-mage-autoload.php b/phpstan-bootstrap-mage-autoload.php deleted file mode 100644 index 92d29f0..0000000 --- a/phpstan-bootstrap-mage-autoload.php +++ /dev/null @@ -1,42 +0,0 @@ -getParameter('magentoRootPath'); -if (empty($magentoRootPath)) { - throw new \Exception('Please set "magentoRootPath" in your phpstan.neon.'); -} - -if (!defined('BP')) { - define('BP', $magentoRootPath); -} - -(new ModuleControllerAutoloader('local'))->register(); -(new ModuleControllerAutoloader('core'))->register(); -(new ModuleControllerAutoloader('community'))->register(); - -/** - * We replace the original Varien_Autoload autoloader with a custom one in order to prevent errors with invalid classes - * that are used throughout the Magento core code. - * The original autoloader would in this case return false and lead to an error in phpstan because the type alias in extension.neon - * is evaluated afterward. - * - * @see \Varien_Autoload::autoload() - */ -spl_autoload_register(static function ($className) { - spl_autoload_unregister([Varien_Autoload::instance(), 'autoload']); - - $classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $className))); - $classFile .= '.php'; - - foreach (explode(':', get_include_path()) as $path) { - if (\file_exists($path . DIRECTORY_SEPARATOR . $classFile)) { - return include $classFile; - } - } -}, true, true); diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php deleted file mode 100644 index 1865ce6..0000000 --- a/phpstan-bootstrap.php +++ /dev/null @@ -1,59 +0,0 @@ -getParameter('magentoRootPath'); -if (empty($magentoRootPath)) { - throw new \Exception('Please set "magentoRootPath" in your phpstan.neon.'); -} - -if (!defined('BP')) { - define('BP', $magentoRootPath); -} - -define('staticReflection', true); - -if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); -} -if (!defined('PS')) { - define('PS', PATH_SEPARATOR); -} - -/** - * Set include path - */ -$paths = []; -$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local'; -$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community'; -$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core'; -$paths[] = BP . DS . 'lib'; - -$appPath = implode(PS, $paths); -set_include_path($appPath . PS . get_include_path()); -include_once 'Mage/Core/functions.php'; - -(new ModuleControllerAutoloader('local'))->register(); -(new ModuleControllerAutoloader('core'))->register(); -(new ModuleControllerAutoloader('community'))->register(); - -/** - * Custom autoloader compatible with Varien_Autoload - * Autoloading is needed only for the PHPStanMagento1\Config\MagentoCore which inherits from some magento classes. - * PHPStan uses static analysis, so doesn't require autoloading. - */ -spl_autoload_register(static function ($className) { - $classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $className))); - $classFile .= '.php'; - - foreach (explode(':', get_include_path()) as $path) { - if (\file_exists($path . DIRECTORY_SEPARATOR . $classFile)) { - return include $classFile; - } - } -}, true, true); diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index 748c8b3..0000000 --- a/phpstan.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - paths: - - src - level: 7 diff --git a/src/Autoload/Magento/ModuleControllerAutoloader.php b/src/Autoload/Magento/ModuleControllerAutoloader.php deleted file mode 100644 index 1de7c84..0000000 --- a/src/Autoload/Magento/ModuleControllerAutoloader.php +++ /dev/null @@ -1,44 +0,0 @@ -codePool = $codePool; - $this->magentoRoot = $magentoRoot; - } - - public function register(): void - { - spl_autoload_register([$this, 'autoload']); - } - - public function autoload(string $className): void - { - if (preg_match('/^([a-zA-Z0-9\x7f-\xff]*)_([a-zA-Z0-9\x7f-\xff]*)_([a-zA-Z0-9_\x7f-\xff]+)/', $className, $match) === 1) { - $class = str_replace('_', '/', $match[3]); - $controllerFilename = sprintf('%s/app/code/%s/%s/%s/controllers/%s.php', $this->magentoRoot, $this->codePool, $match[1], $match[2], $class); - if (file_exists($controllerFilename)) { - (static function ($file) { - include $file; - })($controllerFilename); - } - } - } -} diff --git a/src/Config/MageCoreConfig.php b/src/Config/MageCoreConfig.php new file mode 100644 index 0000000..81407ad --- /dev/null +++ b/src/Config/MageCoreConfig.php @@ -0,0 +1,71 @@ +useLocalXml = $useLocalXml; + } + + public function getConfig(): Mage_Core_Model_Config + { + if ($this->hasInitialized === false && $this->useLocalXml === false) { + $this->hasInitialized = true; + Mage::init('', 'store', ['is_installed' => false]); + } + return Mage::app()->getConfig(); + } + + /** + * @return ?callable(string): (string|false) + */ + public function getClassNameConverterFunction(string $class, string $method): ?callable + { + switch ("$class::$method") { + case 'Mage::getModel': + case 'Mage::getSingleton': + case 'Mage_Core_Model_Config::getModelInstance': + return fn (string $alias) => $this->getConfig()->getModelClassName($alias); + case 'Mage::getResourceModel': + case 'Mage::getResourceSingleton': + case 'Mage_Core_Model_Config::getResourceModelInstance': + return fn (string $alias) => $this->getConfig()->getResourceModelClassName($alias); + case 'Mage::getResourceHelper': + case 'Mage_Core_Model_Config::getResourceHelper': + case 'Mage_Core_Model_Config::getResourceHelperInstance': + return method_exists($this->getConfig(), 'getResourceHelperClassName') + ? fn (string $alias) => $this->getConfig()->getResourceHelperClassName($alias) + : null; + case 'Mage_Core_Model_Layout::createBlock': + case 'Mage_Core_Model_Layout::getBlockSingleton': + return fn (string $alias) => $this->getConfig()->getBlockClassName($alias); + case 'Mage::helper': + case 'Mage_Core_Model_Layout::helper': + case 'Mage_Core_Block_Abstract::helper': + case 'Mage_Core_Model_Config::getHelperInstance': + return fn (string $alias) => $this->getConfig()->getHelperClassName($alias); + case 'Mage_Core_Model_Config::getNodeClassInstance': + return fn (string $path) => $this->getConfig()->getNodeClassName($path); + case 'Mage_Admin_Model_User::_helper': + case 'Mage_Adminhtml_Controller_Rss_Abstract::_helper': + case 'Mage_Api_Model_User::_helper': + case 'Mage_Customer_AccountController::_helper': + case 'Mage_Customer_Model_Customer::_helper': + case 'Mage_Rss_Controller_Abstract::_helper': + case 'Mage_SalesRule_Model_Validator::_helper': + case 'Mage_Weee_Helper_Data::_helper': + case 'Mage_Weee_Model_Config_Source_Fpt_Tax::_helper': + // Deprecated _helper calls + return fn (string $alias) => $this->getConfig()->getHelperClassName($alias); + } + return null; + } +} diff --git a/src/Config/MagentoCore.php b/src/Config/MagentoCore.php deleted file mode 100644 index 62495a7..0000000 --- a/src/Config/MagentoCore.php +++ /dev/null @@ -1,569 +0,0 @@ -_prototype = new \Mage_Core_Model_Config_Base(); - parent::__construct($sourceData); - } - - /** - * workaround to skip calling getOptions(), so we don't have to override - * Mage_Core_Model_Config_Options too - * @return string - */ - protected function getEtcDir() - { - return BP . '/app/etc'; - } - - /** - * workaround to skip calling getOptions(), so we don't have to override - * Mage_Core_Model_Config_Options too - * @return string - */ - protected function getCodeDir() - { - return BP . '/app/code'; - } - - /** - * Retrieve class name by class group - * - * @param string $groupType currently supported model, block, helper - * @param string $classId slash separated class identifier, ex. group/class - * @param string $groupRootNode optional config path for group config - * @return string - */ - public function getGroupedClassName($groupType, $classId, $groupRootNode = null) - { - if (empty($groupRootNode)) { - $groupRootNode = 'global/' . $groupType . 's'; - } - - $classArr = explode('/', trim($classId)); - $group = $classArr[0]; - $class = !empty($classArr[1]) ? $classArr[1] : null; - - if (isset($this->_classNameCache[$groupRootNode][$group][$class])) { - return $this->_classNameCache[$groupRootNode][$group][$class]; - } - - $config = $this->_xml->global->{$groupType . 's'}->{$group}; - - // First - check maybe the entity class was rewritten - $className = ''; - if (isset($config->rewrite->$class)) { - $className = (string)$config->rewrite->$class; - } else { - /** - * Backwards compatibility for pre-MMDB extensions. - * In MMDB release resource nodes <..._mysql4> were renamed to <..._resource>. So is left - * to keep name of previously used nodes, that still may be used by non-updated extensions. - */ - if (isset($config->deprecatedNode)) { - $deprecatedNode = $config->deprecatedNode; - $configOld = $this->_xml->global->{$groupType . 's'}->$deprecatedNode; - if (isset($configOld->rewrite->$class)) { - $className = (string) $configOld->rewrite->$class; - } - } - } - - $className = trim($className); - - // Second - if entity is not rewritten then use class prefix to form class name - if (empty($className)) { - if (!empty($config)) { - $className = $this->getClassName($config); - } - if (empty($className)) { - $className = 'mage_' . $group . '_' . $groupType; - } - if (!empty($class)) { - $className .= '_' . $class; - } - $className = uc_words($className); - } - - $this->_classNameCache[$groupRootNode][$group][$class] = $className; - return $className; - } - - /** - * copied from Mage_Core_Model_Config_Element to avoid calling Mage:: - * - * @param \SimpleXMLElement $config - * @return string|false - */ - public function getClassName(\SimpleXMLElement $config) - { - if ($config->class) { - $model = (string)$config->class; - } elseif ($config->model) { - $model = (string)$config->model; - } else { - return false; - } - return $this->getModelClassName($model); - } - - /** - * Retrieve block class name - * - * @param string $blockType - * @return string - */ - public function getBlockClassName($blockType) - { - if (strpos($blockType, '/') === false) { - return $blockType; - } - return $this->getGroupedClassName('block', $blockType); - } - - /** - * Retrieve helper class name - * - * @param string $helperName - * @return string - */ - public function getHelperClassName($helperName) - { - if (strpos($helperName, '/') === false) { - $helperName .= '/data'; - } - return $this->getGroupedClassName('helper', $helperName); - } - /** - * Retrieve module class name - * - * @param string $modelClass - * @return string - */ - public function getModelClassName($modelClass) - { - $modelClass = trim($modelClass); - if (strpos($modelClass, '/') === false) { - return $modelClass; - } - return $this->getGroupedClassName('model', $modelClass); - } - - /** - * Get factory class name for a resource - * - * @param string $modelClass - * @return string|false - */ - protected function _getResourceModelFactoryClassName($modelClass) - { - $classArray = explode('/', $modelClass); - if (count($classArray) != 2) { - return false; - } - - [$module, $model] = $classArray; - if (!isset($this->_xml->global->models->{$module})) { - return false; - } - - $moduleNode = $this->_xml->global->models->{$module}; - if (!empty($moduleNode->resourceModel)) { - $resourceModel = (string)$moduleNode->resourceModel; - } else { - return false; - } - - return $resourceModel . '/' . $model; - } - - /** - * Get a resource model class name - * - * @param string $modelClass - * @return string|false - */ - public function getResourceModelClassName($modelClass) - { - $factoryName = $this->_getResourceModelFactoryClassName($modelClass); - if ($factoryName) { - return $this->getModelClassName($factoryName); - } - return false; - } - - /** - * Load base system configuration (config.xml and local.xml files) - * - * @return $this - */ - public function loadBase() - { - $etcDir = $this->getEtcDir(); - - $files = glob($etcDir . DS . '*.xml'); - - $this->loadFile(current($files)); - while ($file = next($files)) { - $merge = clone $this->_prototype; - $merge->loadFile($file); - $this->extend($merge); - } - if (in_array($etcDir . DS . 'local.xml', $files)) { - $this->_isLocalConfigLoaded = true; - } - return $this; - } - - /** - * Load modules configuration - * - * @return $this - */ - public function loadModules() - { - $this->_loadDeclaredModules(); - - $this->loadModulesConfiguration(['config.xml'], $this); - - /** - * Prevent local.xml directives overwriting - */ - $mergeConfig = clone $this->_prototype; - $this->_isLocalConfigLoaded = $mergeConfig->loadFile($this->getEtcDir() . DS . 'local.xml'); - if ($this->_isLocalConfigLoaded) { - $this->extend($mergeConfig); - } - - $this->applyExtends(); - return $this; - } - - /** - * Iterate all active modules "etc" folders and combine data from - * specified xml file name to one object - * - * @param array $fileName - * @param null|\Mage_Core_Model_Config_Base $mergeToObject - * @param null $mergeModel - * @return \Mage_Core_Model_Config_Base - */ - public function loadModulesConfiguration(array $fileName, $mergeToObject = null, $mergeModel = null) - { - $disableLocalModules = $this->_disableLocalModules(); - - if ($mergeToObject === null) { - $mergeToObject = clone $this->_prototype; - $mergeToObject->loadString(''); - } - if ($mergeModel === null) { - $mergeModel = clone $this->_prototype; - } - $modules = $this->getNode('modules')->children(); - foreach ($modules as $modName => $module) { - /** @var \Mage_Core_Model_Config_Element $module */ - if ($module->is('active')) { - if ($disableLocalModules && ((string)$module->codePool === 'local')) { - continue; - } - - foreach ($fileName as $configFile) { - $configFile = $this->getModuleDir('etc', $modName) . DS . $configFile; - if ($mergeModel->loadFile($configFile)) { - $this->_makeEventsLowerCase('global', $mergeModel); - $this->_makeEventsLowerCase('frontend', $mergeModel); - $this->_makeEventsLowerCase('admin', $mergeModel); - $this->_makeEventsLowerCase('adminhtml', $mergeModel); - - $mergeToObject->extend($mergeModel, true); - } - } - } - } - return $mergeToObject; - } - - /** - * Get module config node - * - * @param string $moduleName - * @return \Mage_Core_Model_Config_Element|\SimpleXMLElement - */ - public function getModuleConfig($moduleName = '') - { - $modules = $this->getNode('modules'); - if ('' === $moduleName) { - return $modules; - } else { - return $modules->$moduleName; - } - } - - /** - * Get module directory by directory type - * - * @param string $type - * @param string $moduleName - * @return string - */ - public function getModuleDir($type, $moduleName) - { - $codePool = (string)$this->getModuleConfig($moduleName)->codePool; - $dir = $this->getCodeDir() . DS . $codePool . DS . uc_words($moduleName, DS); - - switch ($type) { - case 'etc': - $dir .= DS . 'etc'; - break; - - case 'controllers': - $dir .= DS . 'controllers'; - break; - - case 'sql': - $dir .= DS . 'sql'; - break; - case 'data': - $dir .= DS . 'data'; - break; - - case 'locale': - $dir .= DS . 'locale'; - break; - } - - $dir = str_replace('/', DS, $dir); - return $dir; - } - - /** - * Load declared modules configuration - * - * @param null $mergeConfig depricated - * @return $this|void - */ - protected function _loadDeclaredModules($mergeConfig = null) - { - $moduleFiles = $this->_getDeclaredModuleFiles(); - if (!$moduleFiles) { - return ; - } - - $unsortedConfig = new \Mage_Core_Model_Config_Base(); - $unsortedConfig->loadString(''); - $fileConfig = new \Mage_Core_Model_Config_Base(); - - // load modules declarations - foreach ($moduleFiles as $file) { - $fileConfig->loadFile($file); - $unsortedConfig->extend($fileConfig); - } - - $moduleDepends = []; - foreach ($unsortedConfig->getNode('modules')->children() as $moduleName => $moduleNode) { - $depends = []; - if ($moduleNode->depends) { - foreach ($moduleNode->depends->children() as $depend) { - $depends[$depend->getName()] = true; - } - } - $moduleDepends[$moduleName] = [ - 'module' => $moduleName, - 'depends' => $depends, - 'active' => (string)$moduleNode->active === 'true', - ]; - } - - // check and sort module dependence - $moduleDepends = $this->_sortModuleDepends($moduleDepends); - - // create sorted config - $sortedConfig = new \Mage_Core_Model_Config_Base(); - $sortedConfig->loadString(''); - - foreach ($unsortedConfig->getNode()->children() as $nodeName => $node) { - if ($nodeName !== 'modules') { - $sortedConfig->getNode()->appendChild($node); - } - } - - foreach ($moduleDepends as $moduleProp) { - $node = $unsortedConfig->getNode('modules/' . $moduleProp['module']); - $sortedConfig->getNode('modules')->appendChild($node); - } - - $this->extend($sortedConfig); - - return $this; - } - - /** - * Sort modules and check depends - * - * @param array $modules - * @return array - */ - protected function _sortModuleDepends($modules) - { - foreach ($modules as $moduleName => $moduleProps) { - $depends = $moduleProps['depends']; - foreach ($moduleProps['depends'] as $depend => $true) { - if ($moduleProps['active'] && ((!isset($modules[$depend])) || empty($modules[$depend]['active']))) { - throw new \Exception( - \sprintf('Module "%1$s" requires module "%2$s".', $moduleName, $depend) - ); - } - $depends = array_merge($depends, $modules[$depend]['depends']); - } - $modules[$moduleName]['depends'] = $depends; - } - $modules = array_values($modules); - - $size = count($modules) - 1; - for ($i = $size; $i >= 0; $i--) { - for ($j = $size; $i < $j; $j--) { - if (isset($modules[$i]['depends'][$modules[$j]['module']])) { - $value = $modules[$i]; - $modules[$i] = $modules[$j]; - $modules[$j] = $value; - } - } - } - - $definedModules = []; - foreach ($modules as $moduleProp) { - foreach ($moduleProp['depends'] as $dependModule => $true) { - if (!isset($definedModules[$dependModule])) { - throw new \Exception( - \sprintf('Module "%1$s" cannot depend on "%2$s".', $moduleProp['module'], $dependModule) - ); - } - } - $definedModules[$moduleProp['module']] = true; - } - - return $modules; - } - - /** - * Retrive Declared Module file list - * - * @return array|false - */ - protected function _getDeclaredModuleFiles() - { - $etcDir = $this->getEtcDir(); - $moduleFiles = glob($etcDir . DS . 'modules' . DS . '*.xml'); - - if (!$moduleFiles) { - return false; - } - - $collectModuleFiles = [ - 'base' => [], - 'mage' => [], - 'custom' => [] - ]; - - foreach ($moduleFiles as $v) { - $name = explode(DIRECTORY_SEPARATOR, $v); - $name = substr($name[count($name) - 1], 0, -4); - - if ($name === 'Mage_All') { - $collectModuleFiles['base'][] = $v; - } elseif (substr($name, 0, 5) === 'Mage_') { - $collectModuleFiles['mage'][] = $v; - } else { - $collectModuleFiles['custom'][] = $v; - } - } - - return array_merge( - $collectModuleFiles['base'], - $collectModuleFiles['mage'], - $collectModuleFiles['custom'] - ); - } - - /** - * Makes all events to lower-case - * - * @param string $area - * @param \Varien_Simplexml_Config $mergeModel - */ - protected function _makeEventsLowerCase($area, \Varien_Simplexml_Config $mergeModel) - { - $events = $mergeModel->getNode($area . '/' . \Mage_Core_Model_App_Area::PART_EVENTS); - if ($events !== false) { - $children = clone $events->children(); - /** @var \Mage_Core_Model_Config_Element $event */ - foreach ($children as $event) { - if ($this->_isNodeNameHasUpperCase($event)) { - $oldName = $event->getName(); - $newEventName = strtolower($oldName); - if (!isset($events->$newEventName)) { - /** @var \Mage_Core_Model_Config_Element $newNode */ - - $newNode = $events->addChild($newEventName); - $newNode->extend($event); - } - unset($events->$oldName); - } - } - } - } - - /** - * Checks is event name has upper-case letters - * - * @param \Mage_Core_Model_Config_Element $event - * @return bool - */ - protected function _isNodeNameHasUpperCase(\Mage_Core_Model_Config_Element $event) - { - return (strtolower($event->getName()) !== (string)$event->getName()); - } - - protected function _disableLocalModules(): bool - { - return false; - } -} diff --git a/src/PhpDoc/BindThisScopeResolverExtension.php b/src/PhpDoc/BindThisScopeResolverExtension.php new file mode 100644 index 0000000..d3c8e3e --- /dev/null +++ b/src/PhpDoc/BindThisScopeResolverExtension.php @@ -0,0 +1,89 @@ + $this'; + + public function beforeTraverse(array $nodes) + { + foreach ($nodes as $node) { + if ($node instanceof Node\Stmt\Namespace_) { + $this->beforeTraverse($node->stmts); + break; + } + + if ($node->getDocComment() === null) { + continue; + } + + $comment = preg_replace(self::PHPDOC_PATTERN, self::PHPDOC_REPLACE, $node->getDocComment()->getText(), 1, $match); + + if (is_string($comment) && $match === 1) { + $node->setDocComment(new Comment\Doc($comment)); + break; + } + } + return null; + } + + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type + { + if (!$typeNode instanceof GenericTypeNode) { + return null; + } + + $typeName = $typeNode->type; + if ($typeName->name !== self::GENERIC_TYPE) { + return null; + } + + /** @var IdentifierTypeNode[] */ + $arguments = $typeNode->genericTypes; + if (count($arguments) !== 1) { + return null; + } + + $className = $nameScope->resolveStringName($arguments[0]->name); + + return new class($className) extends ObjectType { + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection + { + return new WrappedExtendedMethodReflection( + new PublicMethodReflection(parent::getMethod($methodName, $scope)) + ); + } + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return new WrappedExtendedPropertyReflection( + new PublicPropertyReflection(parent::getProperty($propertyName, $scope)) + ); + } + }; + } +} diff --git a/src/Reflection/MagicMethodReflection.php b/src/Reflection/MagicMethodReflection.php new file mode 100644 index 0000000..bd543c6 --- /dev/null +++ b/src/Reflection/MagicMethodReflection.php @@ -0,0 +1,104 @@ +originalMethod = $originalMethod; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->originalMethod->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->originalMethod->isStatic(); + } + + public function isPrivate(): bool + { + return $this->originalMethod->isPrivate(); + } + + public function isPublic(): bool + { + return $this->originalMethod->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->originalMethod->getDocComment(); + } + + public function getName(): string + { + return $this->originalMethod->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->originalMethod->getPrototype(); + } + + /** + * @return list + */ + public function getVariants(): array + { + $variant = $this->originalMethod->getVariants()[0]; + return [ + new FunctionVariant( + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + array_slice($variant->getParameters(), 1), + $variant->isVariadic(), + $variant->getReturnType(), + ) + ]; + } + + public function isDeprecated(): TrinaryLogic + { + return $this->originalMethod->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->originalMethod->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->originalMethod->isFinal(); + } + + public function isInternal(): TrinaryLogic + { + return $this->originalMethod->isInternal(); + } + + public function getThrowType(): ?Type + { + return $this->originalMethod->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->originalMethod->hasSideEffects(); + } +} diff --git a/src/Reflection/PublicMethodReflection.php b/src/Reflection/PublicMethodReflection.php new file mode 100644 index 0000000..e058a58 --- /dev/null +++ b/src/Reflection/PublicMethodReflection.php @@ -0,0 +1,94 @@ +originalMethod = $originalMethod; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->originalMethod->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->originalMethod->isStatic(); + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return $this->originalMethod->getDocComment(); + } + + public function getName(): string + { + return $this->originalMethod->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->originalMethod->getPrototype(); + } + + /** + * @return list + */ + public function getVariants(): array + { + return $this->originalMethod->getVariants(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->originalMethod->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->originalMethod->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->originalMethod->isFinal(); + } + + public function isInternal(): TrinaryLogic + { + return $this->originalMethod->isInternal(); + } + + public function getThrowType(): ?Type + { + return $this->originalMethod->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->originalMethod->hasSideEffects(); + } +} diff --git a/src/Reflection/PublicPropertyReflection.php b/src/Reflection/PublicPropertyReflection.php new file mode 100644 index 0000000..651f2f7 --- /dev/null +++ b/src/Reflection/PublicPropertyReflection.php @@ -0,0 +1,83 @@ +originalProperty = $originalProperty; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->originalProperty->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->originalProperty->isStatic(); + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return $this->originalProperty->getDocComment(); + } + + public function getReadableType(): Type + { + return $this->originalProperty->getReadableType(); + } + + public function getWritableType(): Type + { + return $this->originalProperty->getWritableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return $this->originalProperty->canChangeTypeAfterAssignment(); + } + + public function isReadable(): bool + { + return $this->originalProperty->isReadable(); + } + + public function isWritable(): bool + { + return $this->originalProperty->isWritable(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->originalProperty->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->originalProperty->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->originalProperty->isInternal(); + } +} diff --git a/src/Reflection/Varien/Object/MagicMethodReflection.php b/src/Reflection/Varien/Object/MagicMethodReflection.php deleted file mode 100644 index 718e299..0000000 --- a/src/Reflection/Varien/Object/MagicMethodReflection.php +++ /dev/null @@ -1,100 +0,0 @@ -declaringClass = $declaringClass; - $this->name = $name; - } - - public function getDeclaringClass(): ClassReflection - { - return $this->declaringClass; - } - - public function getPrototype(): ClassMemberReflection - { - return $this; - } - - public function isStatic(): bool - { - return false; - } - - public function isPrivate(): bool - { - return false; - } - - public function isPublic(): bool - { - return true; - } - - public function getName(): string - { - return $this->name; - } - - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ - public function getVariants(): array - { - return [ - new TrivialParametersAcceptor(), - ]; - } - - public function getDocComment(): ?string - { - return null; - } - - public function isDeprecated(): \PHPStan\TrinaryLogic - { - return \PHPStan\TrinaryLogic::createMaybe(); - } - - public function getDeprecatedDescription(): ?string - { - return null; - } - - public function isFinal(): \PHPStan\TrinaryLogic - { - return \PHPStan\TrinaryLogic::createNo(); - } - - public function isInternal(): \PHPStan\TrinaryLogic - { - return \PHPStan\TrinaryLogic::createMaybe(); - } - - public function getThrowType(): ?\PHPStan\Type\Type - { - return null; - } - - public function hasSideEffects(): \PHPStan\TrinaryLogic - { - return \PHPStan\TrinaryLogic::createMaybe(); - } -} diff --git a/src/Reflection/Varien/Object/MagicMethodsReflectionExtension.php b/src/Reflection/Varien/Object/MagicMethodsReflectionExtension.php deleted file mode 100644 index 0a09b9f..0000000 --- a/src/Reflection/Varien/Object/MagicMethodsReflectionExtension.php +++ /dev/null @@ -1,25 +0,0 @@ -isSubclassOf(Varien_Object::class) || $classReflection->getName() === Varien_Object::class) - && \in_array(substr($methodName, 0, 3), $magentoMagicMethods, true); - } - - public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection - { - return new MagicMethodReflection($classReflection, $methodName); - } -} diff --git a/src/Reflection/VarienObjectReflectionExtension.php b/src/Reflection/VarienObjectReflectionExtension.php new file mode 100644 index 0000000..97458af --- /dev/null +++ b/src/Reflection/VarienObjectReflectionExtension.php @@ -0,0 +1,57 @@ +enforceDocBlock = $enforceDocBlock; + } + + public function hasMethod(ClassReflection $classReflection, string $methodName): bool + { + if (!in_array(substr($methodName, 0, 3), ['get', 'set', 'uns', 'has'], true)) { + return false; + } + if (!$classReflection->is(Varien_Object::class)) { + return false; + } + + if (isset($classReflection->getMethodTags()[$methodName])) { + return false; + } + + if ($classReflection->isSubclassOf(Varien_Object::class) && $this->enforceDocBlock) { + return false; + } + + return true; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + switch (substr($methodName, 0, 3)) { + case 'get': + return new MagicMethodReflection($classReflection->getNativeMethod('getData')); + case 'set': + return new MagicMethodReflection($classReflection->getNativeMethod('setData')); + case 'uns': + return new MagicMethodReflection($classReflection->getNativeMethod('unsetData')); + case 'has': + return new MagicMethodReflection($classReflection->getNativeMethod('hasData')); + default: + throw new ShouldNotHappenException(); + } + } +} diff --git a/src/Rules/MageInvalidTypeRule.php b/src/Rules/MageInvalidTypeRule.php new file mode 100644 index 0000000..5e514ad --- /dev/null +++ b/src/Rules/MageInvalidTypeRule.php @@ -0,0 +1,101 @@ + + */ +final class MageInvalidTypeRule implements Rule +{ + private ExprPrinter $exprPrinter; + private MageCoreConfig $mageCoreConfig; + + public function __construct(ExprPrinter $exprPrinter, MageCoreConfig $mageCoreConfig) + { + $this->exprPrinter = $exprPrinter; + $this->mageCoreConfig = $mageCoreConfig; + } + + public function getNodeType(): string + { + return Node\Expr\CallLike::class; + } + + public function processNode(Node $methodCall, Scope $scope): array + { + if (!$methodCall instanceof Node\Expr\MethodCall && !$methodCall instanceof Node\Expr\StaticCall) { + return []; + } + if (!$methodCall->name instanceof Node\Identifier) { + return []; + } + if (count($methodCall->getArgs()) === 0) { + return []; + } + + if ($methodCall instanceof Node\Expr\MethodCall) { + $calledOnType = $scope->getType($methodCall->var); + } elseif ($methodCall instanceof Node\Expr\StaticCall) { + if ($methodCall->class instanceof Node\Name) { + $calledOnType = $scope->resolveTypeByName($methodCall->class); + } else { + $calledOnType = $scope->getType($methodCall->class); + } + } + + $methodReflection = $scope->getMethodReflection($calledOnType, $methodCall->name->toString()); + + if ($methodReflection === null) { + return []; + } + + $fn = $this->mageCoreConfig->getClassNameConverterFunction( + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName() + ); + + if (!is_callable($fn)) { + return []; + } + + $aliases = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + + $invalidTypes = []; + + foreach ($aliases as $alias) { + + $className = $fn($alias->getValue()); + + if ($className === false) { + $invalidTypes[] = 'bool(false)'; + } elseif (class_exists($className) === false) { + $invalidTypes[] = $className; + } + } + + if (count($invalidTypes) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s resulted in invalid type %s.', + ltrim($this->exprPrinter->printExpr($methodCall), '\\'), + implode('|', $invalidTypes), + ))->identifier('mage.invalidType')->build() + ]; + } +} diff --git a/src/Type/Mage/CoreBlockAbstract/Helper.php b/src/Type/Mage/CoreBlockAbstract/Helper.php deleted file mode 100644 index 61eaa3e..0000000 --- a/src/Type/Mage/CoreBlockAbstract/Helper.php +++ /dev/null @@ -1,18 +0,0 @@ -getMagentoConfig()->getHelperClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'helper'; - } -} diff --git a/src/Type/Mage/CoreBlockAbstract/MethodReturnTypeDetector.php b/src/Type/Mage/CoreBlockAbstract/MethodReturnTypeDetector.php deleted file mode 100644 index 2cf2a50..0000000 --- a/src/Type/Mage/CoreBlockAbstract/MethodReturnTypeDetector.php +++ /dev/null @@ -1,13 +0,0 @@ -getMagentoConfig()->getBlockClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'getBlockSingleton'; - } -} diff --git a/src/Type/Mage/CoreModelLayout/Helper.php b/src/Type/Mage/CoreModelLayout/Helper.php deleted file mode 100644 index 01049d8..0000000 --- a/src/Type/Mage/CoreModelLayout/Helper.php +++ /dev/null @@ -1,18 +0,0 @@ -getMagentoConfig()->getHelperClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'helper'; - } -} diff --git a/src/Type/Mage/CoreModelLayout/MethodReturnTypeDetector.php b/src/Type/Mage/CoreModelLayout/MethodReturnTypeDetector.php deleted file mode 100644 index b24d5d2..0000000 --- a/src/Type/Mage/CoreModelLayout/MethodReturnTypeDetector.php +++ /dev/null @@ -1,13 +0,0 @@ -getMagentoConfig()->getModelClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'getModel'; - } -} diff --git a/src/Type/Mage/GetResourceModel.php b/src/Type/Mage/GetResourceModel.php deleted file mode 100644 index 12d913e..0000000 --- a/src/Type/Mage/GetResourceModel.php +++ /dev/null @@ -1,25 +0,0 @@ -getMagentoConfig()->getResourceModelClassName($identifier); - if ($className === false) { - throw new \PHPStan\Broker\ClassNotFoundException($identifier); - } - return $className; - } - - protected static function getMethodName(): string - { - return 'getResourceModel'; - } -} diff --git a/src/Type/Mage/GetResourceSingleton.php b/src/Type/Mage/GetResourceSingleton.php deleted file mode 100644 index 3150524..0000000 --- a/src/Type/Mage/GetResourceSingleton.php +++ /dev/null @@ -1,18 +0,0 @@ -getMagentoConfig()->getResourceModelClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'getResourceSingleton'; - } -} diff --git a/src/Type/Mage/GetSingleton.php b/src/Type/Mage/GetSingleton.php deleted file mode 100644 index 05b6e0f..0000000 --- a/src/Type/Mage/GetSingleton.php +++ /dev/null @@ -1,18 +0,0 @@ -getMagentoConfig()->getModelClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'getSingleton'; - } -} diff --git a/src/Type/Mage/Helper.php b/src/Type/Mage/Helper.php deleted file mode 100644 index bdd5a28..0000000 --- a/src/Type/Mage/Helper.php +++ /dev/null @@ -1,18 +0,0 @@ -getMagentoConfig()->getHelperClassName($identifier); - } - - protected static function getMethodName(): string - { - return 'helper'; - } -} diff --git a/src/Type/Mage/MethodReturnTypeDetector.php b/src/Type/Mage/MethodReturnTypeDetector.php deleted file mode 100644 index deb944d..0000000 --- a/src/Type/Mage/MethodReturnTypeDetector.php +++ /dev/null @@ -1,81 +0,0 @@ -getName() === $this::getMethodName(); - } - - /** - * @throws \PHPStan\ShouldNotHappenException - */ - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return $this->getTypeFromExpr($methodReflection, $methodCall, $scope); - } - - /** - * @param MethodCall|\PhpParser\Node\Expr\StaticCall $methodCall - */ - protected function getTypeFromExpr(MethodReflection $methodReflection, $methodCall, Scope $scope): Type - { - $argument = $methodCall->getArgs()[0] ?? null; - if ($argument === null || ! $argument->value instanceof String_) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); - } - - $modelName = $argument->value->value; - $modelClassName = $this->getMagentoClassName($modelName); - - return new ObjectType($modelClassName); - } - - /** - * Load Magento XML configuration - * - * @return MagentoCore|\Mage_Core_Model_Config - */ - protected function getMagentoConfig() - { - if (self::$config) { - return self::$config; - } - - //change this to DI of staticReflection config - if (\defined('staticReflection')) { - $config = new MagentoCore(); - $config->loadBase(); - $config->loadModules(); - } else { - $config = \Mage::app()->getConfig(); - } - self::$config = $config; - return self::$config; - } -} diff --git a/src/Type/Mage/StaticMethodReturnTypeDetector.php b/src/Type/Mage/StaticMethodReturnTypeDetector.php deleted file mode 100644 index 106f967..0000000 --- a/src/Type/Mage/StaticMethodReturnTypeDetector.php +++ /dev/null @@ -1,32 +0,0 @@ -isMethodSupported($methodReflection); - } - - /** - * @throws \PHPStan\ShouldNotHappenException - */ - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type - { - return $this->getTypeFromExpr($methodReflection, $methodCall, $scope); - } -} diff --git a/src/Type/MageTypeExtension.php b/src/Type/MageTypeExtension.php new file mode 100644 index 0000000..2da2c51 --- /dev/null +++ b/src/Type/MageTypeExtension.php @@ -0,0 +1,99 @@ +className = $className; + $this->mageCoreConfig = $mageCoreConfig; + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + $fn = $this->mageCoreConfig->getClassNameConverterFunction( + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName() + ); + + return is_callable($fn); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, CallLike $methodCall, Scope $scope): Type + { + if (count($methodCall->getArgs()) === 0) { + return new ConstantBooleanType(false); + } + + $fn = $this->mageCoreConfig->getClassNameConverterFunction( + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName() + ); + + if (!is_callable($fn)) { + throw new ShouldNotHappenException(); + } + + $aliases = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + + $returnTypes = []; + + foreach ($aliases as $alias) { + + $className = $fn($alias->getValue()); + + if ($className === false || class_exists($className) === false) { + $returnTypes[] = new ConstantBooleanType(false); + } else { + $returnTypes[] = new ObjectType($className); + } + } + + if (count($returnTypes) === 0) { + $returnTypes[] = $methodReflection->getVariants()[0]->getReturnType(); + } + + return TypeCombinator::union(...$returnTypes); + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $this->isMethodSupported($methodReflection); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, CallLike $methodCall, Scope $scope): Type + { + return $this->getTypeFromMethodCall($methodReflection, $methodCall, $scope); + } +} diff --git a/stubs/mock.stub b/stubs/mock.stub new file mode 100644 index 0000000..6b04333 --- /dev/null +++ b/stubs/mock.stub @@ -0,0 +1,112 @@ +