ObsoleteCodeTest.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. /**
  7. * Tests to find various obsolete code usage
  8. * (deprecated and removed Magento 1 legacy methods, properties, classes, etc.)
  9. */
  10. namespace Magento\Test\Legacy;
  11. use Magento\Framework\App\Utility\AggregateInvoker;
  12. use Magento\Framework\App\Utility\Files;
  13. use Magento\Framework\Component\ComponentRegistrar;
  14. use Magento\TestFramework\Utility\ChangedFiles;
  15. /**
  16. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  17. */
  18. class ObsoleteCodeTest extends \PHPUnit\Framework\TestCase
  19. {
  20. /**@#+
  21. * Lists of obsolete entities from fixtures
  22. *
  23. * @var array
  24. */
  25. protected static $_classes = [];
  26. protected static $_constants = [];
  27. protected static $_methods = [];
  28. protected static $_attributes = [];
  29. protected static $_namespaces = [];
  30. protected static $_paths = [];
  31. /**#@-*/
  32. /**
  33. * Read fixtures into memory as arrays
  34. */
  35. public static function setUpBeforeClass()
  36. {
  37. $errors = [];
  38. self::_populateList(self::$_classes, $errors, 'obsolete_classes*.php', false);
  39. self::_populateList(self::$_constants, $errors, 'obsolete_constants*.php');
  40. self::_populateList(self::$_methods, $errors, 'obsolete_methods*.php');
  41. self::_populateList(self::$_paths, $errors, 'obsolete_paths*.php', false);
  42. self::_populateList(self::$_namespaces, $errors, 'obsolete_namespaces*.php', false);
  43. self::_populateList(self::$_attributes, $errors, 'obsolete_properties*.php');
  44. if ($errors) {
  45. $message = 'Duplicate patterns identified in list declarations:' . PHP_EOL . PHP_EOL;
  46. foreach ($errors as $file => $list) {
  47. $message .= $file . PHP_EOL;
  48. foreach ($list as $key) {
  49. $message .= " {$key}" . PHP_EOL;
  50. }
  51. $message .= PHP_EOL;
  52. }
  53. throw new \Exception($message);
  54. }
  55. }
  56. /**
  57. * Read the specified file pattern and merge it with the list
  58. *
  59. * Duplicate entries will be recorded into errors array.
  60. *
  61. * @param array $list
  62. * @param array $errors
  63. * @param string $filePattern
  64. * @param bool $hasScope
  65. */
  66. protected static function _populateList(array &$list, array &$errors, $filePattern, $hasScope = true)
  67. {
  68. foreach (glob(__DIR__ . '/_files/' . $filePattern) as $file) {
  69. $readList = include $file;
  70. foreach ($readList as $row) {
  71. list($item, $scope, $replacement, $isDeprecated) = self::_padRow($row, $hasScope);
  72. $key = "{$item}|{$scope}";
  73. if (isset($list[$key])) {
  74. $errors[$file][] = $key;
  75. } else {
  76. $list[$key] = [$item, $scope, $replacement, $isDeprecated];
  77. }
  78. }
  79. }
  80. }
  81. /**
  82. * Populate insufficient row elements regarding to whether the row supposed to have scope value
  83. *
  84. * @param array $row
  85. * @param bool $hasScope
  86. * @return array
  87. */
  88. protected static function _padRow($row, $hasScope)
  89. {
  90. if ($hasScope) {
  91. return array_pad($row, 4, '');
  92. }
  93. list($item, $replacement) = array_pad($row, 2, '');
  94. return [$item, '', $replacement, ''];
  95. }
  96. public function testPhpFiles()
  97. {
  98. $invoker = new AggregateInvoker($this);
  99. $changedFiles = ChangedFiles::getPhpFiles(__DIR__ . '/../_files/changed_files*');
  100. $blacklistFiles = $this->getBlacklistFiles();
  101. foreach ($blacklistFiles as $blacklistFile) {
  102. unset($changedFiles[$blacklistFile]);
  103. }
  104. $invoker(
  105. function ($file) {
  106. $content = file_get_contents($file);
  107. $this->_testObsoleteClasses($content);
  108. $this->_testObsoleteNamespaces($content);
  109. $this->_testObsoleteMethods($content, $file);
  110. $this->_testGetChildSpecialCase($content, $file);
  111. $this->_testGetOptionsSpecialCase($content);
  112. $this->_testObsoleteMethodArguments($content);
  113. $this->_testObsoleteProperties($content);
  114. $this->_testObsoleteActions($content);
  115. $this->_testObsoleteConstants($content);
  116. $this->_testObsoletePropertySkipCalculate($content);
  117. },
  118. $changedFiles
  119. );
  120. }
  121. public function testClassFiles()
  122. {
  123. $invoker = new AggregateInvoker($this);
  124. $invoker(
  125. function ($file) {
  126. $this->_testObsoletePaths($file);
  127. },
  128. Files::init()->getPhpFiles()
  129. );
  130. }
  131. public function testTemplateMageCalls()
  132. {
  133. $invoker = new AggregateInvoker($this);
  134. $invoker(
  135. function ($file) {
  136. $content = file_get_contents($file);
  137. $this->_assertNotRegExp(
  138. '/\bMage::(\w+?)\(/iS',
  139. $content,
  140. "Static Method of 'Mage' class is obsolete."
  141. );
  142. },
  143. Files::init()->getPhpFiles(
  144. Files::INCLUDE_TEMPLATES
  145. | Files::INCLUDE_TESTS
  146. | Files::AS_DATA_SET
  147. )
  148. );
  149. }
  150. public function testXmlFiles()
  151. {
  152. $invoker = new AggregateInvoker($this);
  153. $invoker(
  154. function ($file) {
  155. $content = file_get_contents($file);
  156. $this->_testObsoleteClasses($content, $file);
  157. $this->_testObsoleteNamespaces($content);
  158. $this->_testObsoletePaths($file);
  159. },
  160. Files::init()->getXmlFiles()
  161. );
  162. }
  163. public function testJsFiles()
  164. {
  165. $invoker = new AggregateInvoker($this);
  166. $invoker(
  167. function ($file) {
  168. $content = file_get_contents($file);
  169. $this->_testObsoletePropertySkipCalculate($content);
  170. },
  171. Files::init()->getJsFiles()
  172. );
  173. }
  174. /**
  175. * Assert that obsolete classes are not used in the content
  176. *
  177. * @param string $content
  178. */
  179. protected function _testObsoleteClasses($content)
  180. {
  181. /* avoid collision between obsolete class name and valid namespace and package tag */
  182. $content = preg_replace('/namespace[^;]+;/', '', $content);
  183. $content = preg_replace('/\@package\s[a-zA-Z0-9\\\_]+/', '', $content);
  184. foreach (self::$_classes as $row) {
  185. list($class, , $replacement) = $row;
  186. $this->_assertNotRegExp(
  187. '/[^a-z\d_]' . preg_quote($class, '/') . '[^a-z\d_\\\\]/iS',
  188. $content,
  189. $this->_suggestReplacement(sprintf("Class '%s' is obsolete.", $class), $replacement)
  190. );
  191. }
  192. }
  193. /**
  194. * Assert that obsolete classes are not used in the content
  195. *
  196. * @param string $content
  197. */
  198. protected function _testObsoleteNamespaces($content)
  199. {
  200. foreach (self::$_namespaces as $row) {
  201. list($namespace, , $replacement) = $row;
  202. $this->_assertNotRegExp(
  203. '/namespace\s+' . preg_quote($namespace, '/') . ';/S',
  204. $content,
  205. $this->_suggestReplacement(sprintf("Namespace '%s' is obsolete.", $namespace), $replacement)
  206. );
  207. $this->_assertNotRegExp(
  208. '/[^a-zA-Z\d_]' . preg_quote($namespace . '\\', '/') . '/S',
  209. $content,
  210. $this->_suggestReplacement(sprintf("Namespace '%s' is obsolete.", $namespace), $replacement)
  211. );
  212. }
  213. }
  214. /**
  215. * Assert that obsolete methods or functions are not used in the content
  216. *
  217. * If class context is not specified, declaration/invocation of all functions or methods (of any class)
  218. * will be matched across the board
  219. *
  220. * If context is specified, only the methods will be matched as follows:
  221. * - usage of class::method
  222. * - usage of $this, self and static within the class and its descendants
  223. *
  224. * @param string $content
  225. * @param string $file
  226. */
  227. protected function _testObsoleteMethods($content, $file)
  228. {
  229. foreach (self::$_methods as $row) {
  230. list($method, $class, $replacement, $isDeprecated) = $row;
  231. $quotedMethod = preg_quote($method, '/');
  232. if ($class) {
  233. $message = $this->_suggestReplacement(
  234. "Method '{$class}::{$method}()' is obsolete in file '{$file}'.",
  235. $replacement
  236. );
  237. // without opening parentheses to match static callbacks notation
  238. $this->_assertNotRegExp(
  239. '/' . preg_quote($class, '/') . '::\s*' . $quotedMethod . '[^a-z\d_]/iS',
  240. $content,
  241. $message
  242. );
  243. if ($this->_isClassOrInterface($content, $class) || $this->_isDirectDescendant($content, $class)) {
  244. if (!$isDeprecated) {
  245. $this->_assertNotRegExp('/function\s*' . $quotedMethod . '\s*\(/iS', $content, $message);
  246. }
  247. $this->_assertNotRegExp('/this->' . $quotedMethod . '\s*\(/iS', $content, $message);
  248. $this->_assertNotRegExp(
  249. '/(self|static|parent)::\s*' . $quotedMethod . '\s*\(/iS',
  250. $content,
  251. $message
  252. );
  253. }
  254. } else {
  255. $message = $this->_suggestReplacement(
  256. "Function or method '{$method}()' is obsolete in file '{$file}'.",
  257. $replacement
  258. );
  259. $this->_assertNotRegExp(
  260. '/(?<!public|protected|private|static)\s+function\s*' . $quotedMethod . '\s*\(/iS',
  261. $content,
  262. $message
  263. );
  264. $this->_assertNotRegExp(
  265. '/(?<![a-z\d_:]|->|function\s)' . $quotedMethod . '\s*\(/iS',
  266. $content,
  267. $message
  268. );
  269. }
  270. }
  271. }
  272. /**
  273. * Assert that obsolete paths are not used in the content
  274. *
  275. * This method will search the content for references to class
  276. * that start with obsolete namespace
  277. *
  278. * @param string $file
  279. */
  280. protected function _testObsoletePaths($file)
  281. {
  282. foreach (self::$_paths as $row) {
  283. list($obsoletePath, , $replacementPath) = $row;
  284. $relativePath = str_replace(BP, '', $file);
  285. $message = $this->_suggestReplacement(
  286. "Path '{$obsoletePath}' is obsolete.",
  287. $replacementPath
  288. );
  289. $this->assertStringStartsNotWith($obsoletePath . '/', $relativePath, $message);
  290. $this->assertStringStartsNotWith($obsoletePath . '.', $relativePath, $message);
  291. $this->assertStringStartsNotWith($obsoletePath . 'Factory.', $relativePath, $message);
  292. $this->assertStringStartsNotWith($obsoletePath . 'Interface.', $relativePath, $message);
  293. $this->assertStringStartsNotWith($obsoletePath . 'Test.', $relativePath, $message);
  294. }
  295. }
  296. /**
  297. * Special case: don't allow usage of getChild() method anywhere within app directory
  298. *
  299. * In Magento 1.x it used to belong only to abstract block (therefore all blocks)
  300. * At the same time, the name is pretty generic and can be encountered in other directories, such as lib
  301. *
  302. * @param string $content
  303. * @param string $file
  304. */
  305. protected function _testGetChildSpecialCase($content, $file)
  306. {
  307. $componentRegistrar = new ComponentRegistrar();
  308. foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $modulePath) {
  309. if (0 === strpos($file, $modulePath)) {
  310. $this->_assertNotRegexp(
  311. '/[^a-z\d_]getChild\s*\(/iS',
  312. $content,
  313. 'Block method getChild() is obsolete. ' .
  314. 'Replacement suggestion: \Magento\Framework\View\Element\AbstractBlock::getChildBlock()'
  315. );
  316. }
  317. }
  318. }
  319. /**
  320. * Special case for ->getConfig()->getOptions()->
  321. *
  322. * @param string $content
  323. */
  324. protected function _testGetOptionsSpecialCase($content)
  325. {
  326. $this->_assertNotRegexp(
  327. '/getOptions\(\)\s*->get(Base|App|Code|Design|Etc|Lib|Locale|Js|Media' .
  328. '|Var|Tmp|Cache|Log|Session|Upload|Export)?Dir\(/S',
  329. $content,
  330. 'The class \Magento\Core\Model\Config\Options is obsolete. '
  331. . 'Replacement suggestion: \Magento\Framework\Filesystem'
  332. );
  333. }
  334. /**
  335. * @param string $content
  336. */
  337. protected function _testObsoleteMethodArguments($content)
  338. {
  339. $this->_assertNotRegExp(
  340. '/[^a-z\d_]getTypeInstance\s*\(\s*[^\)]+/iS',
  341. $content,
  342. 'Backwards-incompatible change: method getTypeInstance() is not supposed to be invoked with any arguments.'
  343. );
  344. $this->_assertNotRegExp(
  345. '/\->getUsedProductIds\(([^\)]+,\s*[^\)]+)?\)/',
  346. $content,
  347. 'Backwards-incompatible change: method getUsedProductIds($product)' .
  348. ' must be invoked with one and only one argument - product model object'
  349. );
  350. $this->_assertNotRegExp(
  351. '#->_setActiveMenu\([\'"]([\w\d/_]+)[\'"]\)#Ui',
  352. $content,
  353. 'Backwards-incompatible change: method _setActiveMenu()' .
  354. ' must be invoked with menu item identifier than xpath for menu item'
  355. );
  356. }
  357. /**
  358. * @param string $content
  359. */
  360. protected function _testObsoleteProperties($content)
  361. {
  362. foreach (self::$_attributes as $row) {
  363. list($attribute, $class, $replacement) = $row;
  364. if ($class) {
  365. if (!$this->_isClassOrInterface($content, $class) && !$this->_isDirectDescendant($content, $class)) {
  366. continue;
  367. }
  368. $fullyQualified = "{$class}::\${$attribute}";
  369. } else {
  370. $fullyQualified = $attribute;
  371. }
  372. $this->_assertNotRegExp(
  373. '/[^a-z\d_]' . preg_quote($attribute, '/') . '[^a-z\d_]/iS',
  374. $content,
  375. $this->_suggestReplacement(sprintf("Class attribute '%s' is obsolete.", $fullyQualified), $replacement)
  376. );
  377. }
  378. }
  379. /**
  380. * @param string $content
  381. */
  382. protected function _testObsoleteActions($content)
  383. {
  384. $suggestion = 'Resizing images upon the client request is obsolete, use server-side resizing instead';
  385. $this->_assertNotRegExp(
  386. '#[^a-z\d_/]catalog/product/image[^a-z\d_/]#iS',
  387. $content,
  388. "Action 'catalog/product/image' is obsolete. {$suggestion}"
  389. );
  390. }
  391. /**
  392. * Assert that obsolete constants are not defined/used in the content
  393. *
  394. * Without class context, only presence of the literal will be checked.
  395. *
  396. * In context of a class, match:
  397. * - fully qualified constant notation (with class)
  398. * - usage with self::/parent::/static:: notation
  399. *
  400. * @param string $content
  401. */
  402. protected function _testObsoleteConstants($content)
  403. {
  404. foreach (self::$_constants as $row) {
  405. list($constant, $class, $replacement) = $row;
  406. if ($class) {
  407. $class = ltrim($class, '\\');
  408. $this->_checkConstantWithFullClasspath($constant, $class, $replacement, $content);
  409. $this->_checkConstantWithClasspath($constant, $class, $replacement, $content);
  410. } else {
  411. $regex = '\b' . preg_quote($constant, '/') . '\b';
  412. $this->_checkExistenceOfObsoleteConstants($regex, '', $content, $constant, $replacement, $class);
  413. }
  414. }
  415. }
  416. /**
  417. * Build regular expression from Obsolete Constants with correspond to contents
  418. *
  419. * @param string $classPartialPath
  420. * @param string $content
  421. * @param string $constant
  422. * @return string
  423. */
  424. private function buildRegExFromObsoleteConstant($classPartialPath, $content, $constant)
  425. {
  426. $regex = preg_quote("{$classPartialPath}::{$constant}");
  427. if ($this->_isClassOrInterface($content, $classPartialPath)) {
  428. $regex .= '|' . $this->_getClassConstantDefinitionRegExp($constant)
  429. . '|' . preg_quote("self::{$constant}", '/')
  430. . '|' . preg_quote("static::{$constant}", '/');
  431. } elseif ($this->_isDirectDescendant($content, $classPartialPath)) {
  432. $regex .= '|' . preg_quote("parent::{$constant}", '/');
  433. if (!$this->_isClassConstantDefined($content, $constant)) {
  434. $regex .= '|' . preg_quote("self::{$constant}", '/') . '|' . preg_quote("static::{$constant}", '/');
  435. }
  436. }
  437. return $regex;
  438. }
  439. /**
  440. * Checks condition of using full classpath in 'use' with 'as' (Example: 'use A\B\C as D')
  441. * where A\B\C is the class where the constant is obsolete
  442. *
  443. * @param string $constant
  444. * @param string $class
  445. * @param string $replacement
  446. * @param string $content
  447. */
  448. private function _checkConstantWithFullClasspath($constant, $class, $replacement, $content)
  449. {
  450. $constantRegex = preg_quote($constant, '/');
  451. $classRegex = preg_quote($class);
  452. $this->_checkExistenceOfObsoleteConstants(
  453. $constantRegex,
  454. $classRegex,
  455. $content,
  456. "{$class}::{$constant}",
  457. $replacement,
  458. $class
  459. );
  460. }
  461. /**
  462. * Check all combinations of classpath with constant
  463. *
  464. * @param string $constant
  465. * @param string $class
  466. * @param string $replacement
  467. * @param string $content
  468. */
  469. private function _checkConstantWithClasspath($constant, $class, $replacement, $content)
  470. {
  471. $classPathParts = explode('\\', $class);
  472. $classPartialPath = '';
  473. for ($count = count($classPathParts), $i = $count - 1; $i >= 0; $i--) {
  474. if ($i === ($count - 1)) {
  475. $classPartialPath = $classPathParts[$i] . $classPartialPath;
  476. } else {
  477. $classPartialPath = $classPathParts[$i] . '\\' . $classPartialPath;
  478. }
  479. $constantRegex = $this->buildRegExFromObsoleteConstant($classPartialPath, $content, $constant);
  480. $regexClassPartialPath = preg_replace('/' . preg_quote($classPartialPath) . '$/', '', $class);
  481. $classRegex = preg_quote($regexClassPartialPath . $classPathParts[$i]);
  482. if ($regexClassPartialPath !== '') {
  483. $classRegex .= '|' . preg_quote(rtrim($regexClassPartialPath, '\\'));
  484. }
  485. // Checks condition when classpath is distributed over namespace and class definition
  486. $classRegexNamespaceClass = '/namespace\s+' . preg_quote('\\') . '?(' . $classRegex . ')(\s|;)(\r?\n)+'
  487. . 'class\s+' . preg_quote('\\') . '?(' . preg_quote(rtrim($classPartialPath, '\\')) . ')\s*/';
  488. $matchNamespaceClass = preg_match($classRegexNamespaceClass, $content);
  489. $constantRegexPartial = '/\b(?P<classWithConst>([a-zA-Z0-9_' . preg_quote('\\') . ']*))('
  490. . preg_quote('::') . ')*' . '(' . preg_quote($constant, '/') . '\b)(\s*|;)/';
  491. $matchConstantPartial = preg_match($constantRegexPartial, $content, $match);
  492. if (($matchNamespaceClass === 1) && ($matchConstantPartial === 1) && ($match['classWithConst'] === '')) {
  493. $this->assertSame(
  494. 0,
  495. 1,
  496. $this->_suggestReplacement(sprintf("Constant '%s' is obsolete.", $constant), $replacement)
  497. );
  498. } else {
  499. $this->_checkExistenceOfObsoleteConstants(
  500. $constantRegex,
  501. $classRegex,
  502. $content,
  503. "{$classPartialPath}::{$constant}",
  504. $replacement,
  505. $class
  506. );
  507. }
  508. }
  509. }
  510. /**
  511. * Check existence of Obsolete Constant in current content
  512. *
  513. * @param string $constantRegex
  514. * @param string $classRegex
  515. * @param string $content
  516. * @param string $constant
  517. * @param string $replacement
  518. * @param string $class
  519. */
  520. private function _checkExistenceOfObsoleteConstants(
  521. $constantRegex,
  522. $classRegex,
  523. $content,
  524. $constant,
  525. $replacement,
  526. $class
  527. ) {
  528. $constantRegexFull = '/\b(?P<constPart>((?P<classWithConst>([a-zA-Z0-9_' . preg_quote('\\') . ']*))('
  529. . preg_quote('::') . ')*' . '(' . $constantRegex . '\b)))(\s*|;)/';
  530. $matchConstant = preg_match_all($constantRegexFull, $content, $matchConstantString);
  531. $result = 0;
  532. if ($matchConstant === 1) {
  533. if ($classRegex !== '') {
  534. $classRegexFull = '/(?P<useOrNamespace>(use|namespace))\s+(?P<classPath>(' . preg_quote('\\')
  535. . '?(' . $classRegex . ')))(\s+as\s+(?P<classAlias>([\w\d_]+)))?(\s|;)/';
  536. $matchClass = preg_match($classRegexFull, $content, $matchClassString);
  537. if ($matchClass === 1) {
  538. if ($matchClassString['classAlias']) {
  539. $result = $this->_checkAliasUseNamespace(
  540. $constantRegex,
  541. $matchConstantString,
  542. $matchClassString,
  543. $class
  544. );
  545. } else {
  546. $result = $this->_checkNoAliasUseNamespace($matchConstantString, $matchClassString, $class);
  547. }
  548. } else {
  549. foreach ($matchConstantString['classWithConst'] as $constantMatch) {
  550. if (trim($constantMatch, '\\') === $class) {
  551. $result = 1;
  552. break;
  553. }
  554. }
  555. }
  556. } else {
  557. $result = 1;
  558. }
  559. }
  560. $this->assertSame(
  561. 0,
  562. $result,
  563. $this->_suggestReplacement(sprintf("Constant '%s' is obsolete.", $constant), $replacement)
  564. );
  565. }
  566. /**
  567. * Check proper usage of 'as' alias in 'use' or 'namespace' in context of constant
  568. *
  569. * @param string $constantRegex
  570. * @param string $matchConstantString
  571. * @param string $matchClassString
  572. * @param string $class
  573. * @return int
  574. */
  575. private function _checkAliasUseNamespace(
  576. $constantRegex,
  577. $matchConstantString,
  578. $matchClassString,
  579. $class
  580. ) {
  581. $foundProperUse = false;
  582. $foundAsComponent = false;
  583. $asComponent = $matchClassString['classAlias'];
  584. foreach ($matchConstantString['constPart'] as $constantMatch) {
  585. $expectedOnlyConst = '/' . $asComponent . preg_quote('::') . $constantRegex . '/';
  586. $expectedConstPartialClass = '/' . $asComponent . preg_quote('\\')
  587. . $constantRegex . '/';
  588. if ((preg_match($expectedOnlyConst, $constantMatch) === 1)
  589. || (preg_match($expectedConstPartialClass, $constantMatch) === 1)) {
  590. $foundAsComponent = true;
  591. }
  592. if (strpos($constantMatch, '::') !== false) {
  593. $foundProperUse = $this->_checkCompletePathOfClass(
  594. $constantMatch,
  595. $matchClassString,
  596. $class,
  597. $foundAsComponent,
  598. $asComponent
  599. );
  600. if ($foundProperUse) {
  601. break;
  602. }
  603. }
  604. }
  605. if ($foundProperUse) {
  606. return 1;
  607. } else {
  608. return 0;
  609. }
  610. }
  611. /**
  612. * Check proper usage of classpath in constant and 'use'/'namespace' when there is no 'as' alias
  613. *
  614. * @param string $matchConstantString
  615. * @param string $matchClassString
  616. * @param string $class
  617. * @return int
  618. */
  619. private function _checkNoAliasUseNamespace(
  620. $matchConstantString,
  621. $matchClassString,
  622. $class
  623. ) {
  624. $foundProperUse = false;
  625. foreach ($matchConstantString['constPart'] as $constantMatch) {
  626. $foundProperUse = $this->_checkCompletePathOfClass(
  627. $constantMatch,
  628. $matchClassString,
  629. $class
  630. );
  631. if ($foundProperUse) {
  632. break;
  633. }
  634. }
  635. if ($foundProperUse) {
  636. return 1;
  637. } else {
  638. return 0;
  639. }
  640. }
  641. /**
  642. * Check if class path with constant and in 'use' or 'namespace' forms complete classpath
  643. *
  644. * @param string $constantMatch
  645. * @param array $matchClassString
  646. * @param string $class
  647. * @param bool $foundAsComponent
  648. * @param string $asComponent
  649. * @return bool
  650. */
  651. private function _checkCompletePathOfClass(
  652. $constantMatch,
  653. $matchClassString,
  654. $class,
  655. $foundAsComponent = false,
  656. $asComponent = ''
  657. ) {
  658. $temp = explode('::', $constantMatch);
  659. $pathWithConst = trim(ltrim(str_replace('\\\\', '\\', $temp[0]), '\\'));
  660. if ($pathWithConst === $class) {
  661. return true;
  662. }
  663. if ($foundAsComponent) {
  664. $pathWithConst = ltrim(preg_replace('/^' . $asComponent . '/', '', $pathWithConst), '\\');
  665. if ($pathWithConst === '') {
  666. return true;
  667. }
  668. }
  669. $pathWithConstParts = explode('\\', $pathWithConst);
  670. $pathInUseNamespace = trim($matchClassString['classPath'], '\\');
  671. $pathInUseNamespaceTruncated = trim(trim(
  672. preg_replace(
  673. '/' . preg_quote($pathWithConstParts[0]) . '$/',
  674. '',
  675. $pathInUseNamespace
  676. ),
  677. '\\'
  678. ));
  679. if ($this->_checkClasspathProperDivisionNoConstantPath(
  680. $pathInUseNamespaceTruncated,
  681. $pathInUseNamespace,
  682. $matchClassString,
  683. $class,
  684. $foundAsComponent
  685. )) {
  686. return true;
  687. } else {
  688. return $this->_checkClasspathProperDivisionWithConstantPath(
  689. $pathInUseNamespaceTruncated,
  690. $pathInUseNamespace,
  691. $pathWithConst,
  692. $class,
  693. $foundAsComponent
  694. );
  695. }
  696. }
  697. /**
  698. * Check if classpath is divided in two places with correct constant name
  699. *
  700. * @param string $pathInUseNamespaceTruncated
  701. * @param string $pathInUseNamespace
  702. * @param array $matchClassString
  703. * @param string $class
  704. * @param bool $foundAsComponent
  705. * @return bool
  706. */
  707. private function _checkClasspathProperDivisionNoConstantPath(
  708. $pathInUseNamespaceTruncated,
  709. $pathInUseNamespace,
  710. $matchClassString,
  711. $class,
  712. $foundAsComponent
  713. ) {
  714. if ($pathInUseNamespaceTruncated === $pathInUseNamespace && $pathInUseNamespaceTruncated !== $class
  715. && ($foundAsComponent || (strpos($matchClassString['useOrNamespace'], 'namespace') !== false))) {
  716. return true;
  717. } else {
  718. return false;
  719. }
  720. }
  721. /**
  722. * Check if classpath is divided in two places with constant properly with or without alias
  723. *
  724. * @param string $pathInUseNamespaceTruncated
  725. * @param string $pathInUseNamespace
  726. * @param string $pathWithConst
  727. * @param string $class
  728. * @param bool $foundAsComponent
  729. * @return bool
  730. */
  731. private function _checkClasspathProperDivisionWithConstantPath(
  732. $pathInUseNamespaceTruncated,
  733. $pathInUseNamespace,
  734. $pathWithConst,
  735. $class,
  736. $foundAsComponent
  737. ) {
  738. if ((($pathInUseNamespaceTruncated . '\\' . $pathWithConst === $class)
  739. && ($pathInUseNamespaceTruncated !== $pathInUseNamespace) && !$foundAsComponent)
  740. || (($pathInUseNamespaceTruncated === $class) && (strpos($pathWithConst, '\\') === false)
  741. && $foundAsComponent)) {
  742. return true;
  743. } else {
  744. return false;
  745. }
  746. }
  747. /**
  748. * Whether a class constant is defined in the content or not
  749. *
  750. * @param string $content
  751. * @param string $constant
  752. * @return bool
  753. */
  754. protected function _isClassConstantDefined($content, $constant)
  755. {
  756. return (bool)preg_match('/' . $this->_getClassConstantDefinitionRegExp($constant) . '/S', $content);
  757. }
  758. /**
  759. * Retrieve a PCRE matching a class constant definition
  760. *
  761. * @param string $constant
  762. * @return string
  763. */
  764. protected function _getClassConstantDefinitionRegExp($constant)
  765. {
  766. return '\bconst\s+' . preg_quote($constant, '/') . '\b';
  767. }
  768. /**
  769. * @param string $content
  770. */
  771. protected function _testObsoletePropertySkipCalculate($content)
  772. {
  773. $this->_assertNotRegExp(
  774. '/[^a-z\d_]skipCalculate[^a-z\d_]/iS',
  775. $content,
  776. "Configuration property 'skipCalculate' is obsolete."
  777. );
  778. }
  779. /**
  780. * Analyze contents to determine whether this is declaration of specified class/interface
  781. *
  782. * @param string $content
  783. * @param string $name
  784. * @return bool
  785. */
  786. protected function _isClassOrInterface($content, $name)
  787. {
  788. $name = preg_quote($name, '/');
  789. return (bool)preg_match('/\b(?:class|interface)\s+' . $name . '\b[^{]*\{/iS', $content);
  790. }
  791. /**
  792. * Analyze contents to determine whether this is a direct descendant of specified class/interface
  793. *
  794. * @param string $content
  795. * @param string $name
  796. * @return bool
  797. */
  798. protected function _isDirectDescendant($content, $name)
  799. {
  800. $name = preg_quote($name, '/');
  801. return (bool)preg_match(
  802. '/\s+extends\s+' . $name . '\b|\s+implements\s+[^{]*\b' . $name . '\b[^{^\\\\]*\{/iS',
  803. $content
  804. );
  805. }
  806. /**
  807. * Append a "suggested replacement" part to the string
  808. *
  809. * @param string $original
  810. * @param string $suggestion
  811. * @return string
  812. */
  813. private function _suggestReplacement($original, $suggestion)
  814. {
  815. if ($suggestion) {
  816. return "{$original} Suggested replacement: {$suggestion}";
  817. }
  818. return $original;
  819. }
  820. /**
  821. * Custom replacement for assertNotRegexp()
  822. *
  823. * In this particular test the original assertNotRegexp() cannot be used
  824. * because of too large text $content, which obfuscates tests output
  825. *
  826. * @param string $regex
  827. * @param string $content
  828. * @param string $message
  829. */
  830. protected function _assertNotRegexp($regex, $content, $message)
  831. {
  832. $this->assertSame(0, preg_match($regex, $content), $message);
  833. }
  834. public function testMageMethodsObsolete()
  835. {
  836. $ignored = $this->getBlacklistFiles(true);
  837. $files = Files::init()->getPhpFiles(
  838. Files::INCLUDE_APP_CODE
  839. | Files::INCLUDE_TESTS
  840. | Files::INCLUDE_DEV_TOOLS
  841. | Files::INCLUDE_LIBS
  842. );
  843. $files = array_map('realpath', $files);
  844. $files = array_diff($files, $ignored);
  845. $files = Files::composeDataSets($files);
  846. $invoker = new AggregateInvoker($this);
  847. $invoker(
  848. /**
  849. * Check absence of obsolete Mage class usages
  850. *
  851. * @param string $file
  852. */
  853. function ($file) {
  854. $this->_assertNotRegExp(
  855. '/[^a-z\d_]Mage\s*::/i',
  856. file_get_contents($file),
  857. '"Mage" class methods are obsolete'
  858. );
  859. },
  860. $files
  861. );
  862. }
  863. /**
  864. * @param string $appPath
  865. * @param string $pattern
  866. * @return array
  867. * @throws \Exception
  868. */
  869. private function processPattern($appPath, $pattern)
  870. {
  871. $files = [];
  872. $relativePathStart = strlen($appPath);
  873. $fileSet = glob($appPath . DIRECTORY_SEPARATOR . $pattern, GLOB_NOSORT);
  874. foreach ($fileSet as $file) {
  875. $files[] = ltrim(substr($file, $relativePathStart), '/');
  876. }
  877. return $files;
  878. }
  879. /**
  880. * Reads list of blacklisted files
  881. *
  882. * @param bool $absolutePath
  883. * @return array
  884. * @throws \Exception
  885. */
  886. private function getBlacklistFiles($absolutePath = false)
  887. {
  888. $blackList = include __DIR__ . '/_files/blacklist/obsolete_mage.php';
  889. $ignored = [];
  890. $appPath = BP;
  891. foreach ($blackList as $file) {
  892. if ($absolutePath) {
  893. $ignored = array_merge($ignored, glob($appPath . DIRECTORY_SEPARATOR . $file, GLOB_NOSORT));
  894. } else {
  895. $ignored = array_merge($ignored, $this->processPattern($appPath, $file));
  896. }
  897. }
  898. return $ignored;
  899. }
  900. }