Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
ClassesTest.php
Go to the documentation of this file.
1 <?php
8 namespace Magento\Test\Integrity;
9 
13 
17 class ClassesTest extends \PHPUnit\Framework\TestCase
18 {
22  private $componentRegistrar;
23 
29  private $existingClasses = [];
30 
34  private static $keywordsBlacklist = ["String", "Array", "Boolean", "Element"];
35 
39  private $referenceBlackList = null;
40 
44  protected function setUp()
45  {
46  $this->componentRegistrar = new ComponentRegistrar();
47  }
48 
49  public function testPhpFiles()
50  {
51  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
52  $invoker(
56  function ($file) {
58  $classes = Classes::getAllMatches(
59  $contents,
60  '/
61  # ::getResourceModel ::getBlockSingleton ::getModel ::getSingleton
62  \:\:get(?:ResourceModel | BlockSingleton | Model | Singleton)?\(\s*[\'"]([a-z\d\\\\]+)[\'"]\s*[\),]
63 
64  # various methods, first argument
65  | \->(?:initReport | addBlock | createBlock
66  | setAttributeModel | setBackendModel | setFrontendModel | setSourceModel | setModel
67  )\(\s*\'([a-z\d\\\\]+)\'\s*[\),]
68 
69  # various methods, second argument
70  | \->add(?:ProductConfigurationHelper | OptionsRenderCfg)\(.+?,\s*\'([a-z\d\\\\]+)\'\s*[\),]
71 
72  # \Mage::helper ->helper
73  | (?:Mage\:\:|\->)helper\(\s*\'([a-z\d\\\\]+)\'\s*\)
74 
75  # misc
76  | function\s_getCollectionClass\(\)\s+{\s+return\s+[\'"]([a-z\d\\\\]+)[\'"]
77  | \'resource_model\'\s*=>\s*[\'"]([a-z\d\\\\]+)[\'"]
78  | (?:_parentResourceModelName | _checkoutType | _apiType)\s*=\s*\'([a-z\d\\\\]+)\'
79  | \'renderer\'\s*=>\s*\'([a-z\d\\\\]+)\'
80  /ix'
81  );
82 
83  // without modifier "i". Starting from capital letter is a significant characteristic of a class name
85  $contents,
86  '/(?:\-> | parent\:\:)(?:_init | setType)\(\s*
87  \'([A-Z][a-z\d][A-Za-z\d\\\\]+)\'(?:,\s*\'([A-Z][a-z\d][A-Za-z\d\\\\]+)\')
88  \s*\)/x',
89  $classes
90  );
91 
92  $this->collectResourceHelpersPhp($contents, $classes);
93 
94  $this->assertClassesExist($classes, $file);
95  },
96  Files::init()->getPhpFiles(
103  )
104  );
105  }
106 
114  private function collectResourceHelpersPhp(string $contents, array &$classes): void
115  {
116  $regex = '/(?:\:\:|\->)getResourceHelper\(\s*\'([a-z\d\\\\]+)\'\s*\)/ix';
117  $matches = Classes::getAllMatches($contents, $regex);
118  foreach ($matches as $moduleName) {
119  $classes[] = "{$moduleName}\\Model\\ResourceModel\\Helper\\Mysql4";
120  }
121  }
122 
123  public function testConfigFiles()
124  {
125  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
126  $invoker(
130  function ($path) {
131  $classes = Classes::collectClassesInConfig(simplexml_load_file($path));
132  $this->assertClassesExist($classes, $path);
133  },
134  Files::init()->getMainConfigFiles()
135  );
136  }
137 
138  public function testLayoutFiles()
139  {
140  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
141  $invoker(
145  function ($path) {
146  $xml = simplexml_load_file($path);
147 
148  $classes = Classes::getXmlNodeValues(
149  $xml,
150  '/layout//*[contains(text(), "\\\\Block\\\\") or contains(text(),
151  "\\\\Model\\\\") or contains(text(), "\\\\Helper\\\\")]'
152  );
154  $xml,
155  '/layout//@helper',
156  'helper'
157  ) as $class) {
158  $classes[] = Classes::getCallbackClass($class);
159  }
161  $xml,
162  '/layout//@module',
163  'module'
164  ) as $module) {
165  $classes[] = str_replace('_', '\\', "{$module}_Helper_Data");
166  }
167  $classes = array_merge($classes, Classes::collectLayoutClasses($xml));
168 
169  $this->assertClassesExist(array_unique($classes), $path);
170  },
171  Files::init()->getLayoutFiles()
172  );
173  }
174 
187  private function assertClassesExist(array $classes, string $path): void
188  {
189  if (!$classes) {
190  return;
191  }
192  $badClasses = [];
193  $badUsages = [];
194  foreach ($classes as $class) {
195  $class = trim($class, '\\');
196  try {
197  if (strrchr($class, '\\') === false and !Classes::isVirtual($class)) {
198  $badUsages[] = $class;
199  continue;
200  } else {
201  $this->assertTrue(
202  isset(
203  $this->existingClasses[$class]
204  ) || Files::init()->classFileExists(
205  $class
206  ) || Classes::isVirtual(
207  $class
209  $class
210  )
211  );
212  }
213  $this->existingClasses[$class] = 1;
214  } catch (\PHPUnit\Framework\AssertionFailedError $e) {
215  $badClasses[] = '\\' . $class;
216  }
217  }
218  if ($badClasses) {
219  $this->fail("Files not found for following usages in {$path}:\n" . implode("\n", $badClasses));
220  }
221  if ($badUsages) {
222  $this->fail("Bad usages of classes in {$path}: \n" . implode("\n", $badUsages));
223  }
224  }
225 
226  public function testClassNamespaces()
227  {
228  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
229  $invoker(
235  function ($file) {
236  $relativePath = str_replace(BP . "/", "", $file);
237  // exceptions made for fixture files from tests
238  if (strpos($relativePath, '/_files/') !== false) {
239  return;
240  }
241 
242  $contents = file_get_contents($file);
243 
244  $classPattern = '/^(abstract\s)?class\s[A-Z][^\s\/]+/m';
245 
246  $classNameMatch = [];
247  $className = null;
248 
249  // if no class declaration found for $file, then skip this file
250  if (preg_match($classPattern, $contents, $classNameMatch) == 0) {
251  return;
252  }
253 
254  $classParts = explode(' ', $classNameMatch[0]);
255  $className = array_pop($classParts);
256  $this->assertClassNamespace($file, $relativePath, $contents, $className);
257  },
258  Files::init()->getPhpFiles()
259  );
260  }
261 
272  private function assertClassNamespace(string $file, string $relativePath, string $contents, string $className): void
273  {
274  $namespacePattern = '/(Magento|Zend)\/[a-zA-Z]+[^\.]+/';
275  $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m';
276 
277  $namespaceMatch = [];
278  $formalNamespaceArray = [];
279  $namespaceFolders = null;
280 
281  // if no namespace pattern found according to the path of the file, skip the file
282  if (preg_match($namespacePattern, $relativePath, $namespaceMatch) == 0) {
283  return;
284  }
285 
286  $namespaceFolders = $namespaceMatch[0];
287  $classParts = explode('/', $namespaceFolders);
288  array_pop($classParts);
289  $expectedNamespace = implode('\\', $classParts);
290 
291  if (preg_match($formalPattern, $contents, $formalNamespaceArray) != 0) {
292  $foundNamespace = substr($formalNamespaceArray[0], 10);
293  $foundNamespace = str_replace('\\', '/', $foundNamespace);
294  $foundNamespace .= '/' . $className;
295  if ($namespaceFolders != null && $foundNamespace != null) {
296  $this->assertEquals(
297  $namespaceFolders,
298  $foundNamespace,
299  "Location of {$file} does not match formal namespace: {$expectedNamespace}\n"
300  );
301  }
302  } else {
303  $this->fail("Missing expected namespace \"{$expectedNamespace}\" for file: {$file}");
304  }
305  }
306 
307  public function testClassReferences()
308  {
309  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
310  $invoker(
314  function ($file) {
315  $relativePath = str_replace(BP, "", $file);
316  // Due to the examples given with the regex patterns, we skip this test file itself
317  if (preg_match(
318  '/\/dev\/tests\/static\/testsuite\/Magento\/Test\/Integrity\/ClassesTest.php$/',
320  )) {
321  return;
322  }
323  $contents = file_get_contents($file);
324  $formalPattern = '/^namespace\s[a-zA-Z]+(\\\\[a-zA-Z0-9]+)*/m';
325  $formalNamespaceArray = [];
326 
327  // Skip the file if the class is not defined using formal namespace
328  if (preg_match($formalPattern, $contents, $formalNamespaceArray) == 0) {
329  return;
330  }
331  $namespacePath = str_replace('\\', '/', substr($formalNamespaceArray[0], 10));
332 
333  // Instantiation of new object, for example: "return new Foo();"
334  $newObjectPattern = '/^' .
335  '.*new\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\(.*\)' .
336  '|.*new\s(?<badClass>[A-Z][a-zA-Z0-9]+[a-zA-Z0-9_\\\\]*)\(.*\)\;' .
337  '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
338  '/m';
339  $result1 = [];
340  preg_match_all($newObjectPattern, $contents, $result1);
341 
342  // Static function/variable, for example: "Foo::someStaticFunction();"
343  $staticCallPattern = '/^' .
344  '((?!Magento).)*(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)\:\:.*\;' .
345  '|[^\\\\^a-z^A-Z^0-9^_^:](?<badClass>[A-Z][a-zA-Z0-9_]+)\:\:.*\;' .
346  '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
347  '/m';
348  $result2 = [];
349  preg_match_all($staticCallPattern, $contents, $result2);
350 
351  // Annotation, for example: "* @return \Magento\Foo\Bar" or "* @throws Exception" or "* @return Foo"
352  $annotationPattern = '/^' .
353  '[\s]*\*\s\@(?:return|throws)\s(?<venderClass>\\\\Magento(?:\\\\[a-zA-Z0-9_]+)+)' .
354  '|[\s]*\*\s\@return\s(?<badClass>[A-Z][a-zA-Z0-9_\\\\]+)' .
355  '|[\s]*\*\s\@throws\s(?<exception>[A-Z][a-zA-Z0-9_\\\\]+)' .
356  '|use [A-Z][a-zA-Z0-9_\\\\]+ as (?<aliasClass>[A-Z][a-zA-Z0-9]+)' .
357  '/m';
358  $result3 = [];
359  preg_match_all($annotationPattern, $contents, $result3);
360 
361  $vendorClasses = array_unique(
362  array_merge_recursive($result1['venderClass'], $result2['venderClass'], $result3['venderClass'])
363  );
364 
365  $badClasses = array_unique(
366  array_merge_recursive($result1['badClass'], $result2['badClass'], $result3['badClass'])
367  );
368 
369  $aliasClasses = array_unique(
370  array_merge_recursive($result1['aliasClass'], $result2['aliasClass'], $result3['aliasClass'])
371  );
372 
373  $vendorClasses = array_filter($vendorClasses, 'strlen');
374  $vendorClasses = $this->referenceBlacklistFilter($vendorClasses);
375  if (!empty($vendorClasses)) {
376  $this->assertClassesExist($vendorClasses, $file);
377  }
378 
379  if (!empty($result3['exception']) && $result3['exception'][0] != "") {
380  $badClasses = array_merge($badClasses, array_filter($result3['exception'], 'strlen'));
381  }
382 
383  $badClasses = array_filter($badClasses, 'strlen');
384  if (empty($badClasses)) {
385  return;
386  }
387 
388  $aliasClasses = array_filter($aliasClasses, 'strlen');
389  if (!empty($aliasClasses)) {
390  $badClasses = $this->handleAliasClasses($aliasClasses, $badClasses);
391  }
392 
393  $badClasses = $this->referenceBlacklistFilter($badClasses);
394  $badClasses = $this->removeSpecialCases($badClasses, $file, $contents, $namespacePath);
395  $this->assertClassReferences($badClasses, $file);
396  },
397  Files::init()->getPhpFiles()
398  );
399  }
400 
408  private function handleAliasClasses(array $aliasClasses, array $badClasses): array
409  {
410  foreach ($aliasClasses as $aliasClass) {
411  foreach ($badClasses as $badClass) {
412  if (strpos($badClass, $aliasClass) === 0) {
413  unset($badClasses[array_search($badClass, $badClasses)]);
414  }
415  }
416  }
417 
418  return $badClasses;
419  }
420 
427  private function referenceBlacklistFilter(array $classes): array
428  {
429  // exceptions made for the files from the blacklist
430  $classes = $this->getReferenceBlacklist();
431  foreach ($classes as $class) {
432  if (in_array($class, $this->referenceBlackList)) {
433  unset($classes[array_search($class, $classes)]);
434  }
435  }
436 
437  return $classes;
438  }
439 
445  private function getReferenceBlacklist(): array
446  {
447  if (!isset($this->referenceBlackList)) {
448  $this->referenceBlackList = file(
449  __DIR__ . '/_files/blacklist/reference.txt',
450  FILE_IGNORE_NEW_LINES
451  );
452  }
453 
454  return $this->referenceBlackList;
455  }
456 
466  private function removeSpecialCases(array $badClasses, string $file, string $contents, string $namespacePath): array
467  {
468  foreach ($badClasses as $badClass) {
469  // Remove valid usages of Magento modules from the list
470  // for example: 'Magento_Sales::actions_edit'
471  if (preg_match('/^[A-Z][a-z]+_[A-Z0-9][a-z0-9]+$/', $badClass)) {
472  $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $badClass);
473  if ($moduleDir !== null) {
474  unset($badClasses[array_search($badClass, $badClasses)]);
475  continue;
476  }
477  }
478 
479  // Remove usage of key words such as "Array", "String", and "Boolean"
480  if (in_array($badClass, self::$keywordsBlacklist)) {
481  unset($badClasses[array_search($badClass, $badClasses)]);
482  continue;
483  }
484 
485  $classParts = explode('/', $file);
486  $className = array_pop($classParts);
487  // Remove usage of the class itself from the list
488  if ($badClass . '.php' == $className) {
489  unset($badClasses[array_search($badClass, $badClasses)]);
490  continue;
491  }
492 
493  if ($this->removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, $badClasses, $badClass)) {
494  continue;
495  }
496 
497  $referenceFile = implode('/', $classParts) . '/' . str_replace('\\', '/', $badClass) . '.php';
498  if (file_exists($referenceFile)) {
499  unset($badClasses[array_search($badClass, $badClasses)]);
500  continue;
501  }
502 
503  // Remove usage of classes that have been declared as "use" or "include"
504  // Also deals with case like: "use \Zend\Code\Scanner\FileScanner, Magento\Tools\Di\Compiler\Log\Log;"
505  // (continued) where there is a comma separating two different classes.
506  if (preg_match('/use\s.*[\\n]?.*' . str_replace('\\', '\\\\', $badClass) . '[\,\;]/', $contents)) {
507  unset($badClasses[array_search($badClass, $badClasses)]);
508  continue;
509  }
510  }
511 
512  return $badClasses;
513  }
514 
524  private function removeSpecialCasesNonFullyQualifiedClassNames($namespacePath, &$badClasses, $badClass)
525  {
526  $namespaceParts = explode('/', $namespacePath);
527  $moduleDir = null;
528  if (isset($namespaceParts[1])) {
529  $moduleName = array_shift($namespaceParts) . '_' . array_shift($namespaceParts);
530  $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName);
531  }
532  if ($moduleDir) {
533  $fullPath = $moduleDir . '/' . implode('/', $namespaceParts) . '/' .
534  str_replace('\\', '/', $badClass) . '.php';
535 
536  if (file_exists($fullPath)) {
537  unset($badClasses[array_search($badClass, $badClasses)]);
538  return true;
539  }
540  }
541 
542  $fullPath = $this->getLibraryDirByPath($namespacePath, $badClass);
543 
544  if ($fullPath && file_exists($fullPath)) {
545  unset($badClasses[array_search($badClass, $badClasses)]);
546  return true;
547  } else {
548  return $this->removeSpecialCasesForAllOthers($namespacePath, $badClass, $badClasses);
549  }
550  }
551 
559  private function getLibraryDirByPath(string $namespacePath, string $badClass)
560  {
561  $libraryDir = null;
562  $fullPath = null;
563  $namespaceParts = explode('/', $namespacePath);
564  if (isset($namespaceParts[1]) && $namespaceParts[1]) {
565  $vendor = array_shift($namespaceParts);
566  $lib = array_shift($namespaceParts);
567  if ($lib == 'framework') {
568  $subLib = $namespaceParts[0];
569  $subLib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $subLib));
570  $libraryName = $vendor . '/' . $lib . '-' . $subLib;
571  $libraryDir = $this->componentRegistrar->getPath(
573  strtolower($libraryName)
574  );
575  if ($libraryDir) {
576  array_shift($namespaceParts);
577  } else {
578  $libraryName = $vendor . '/' . $lib;
579  $libraryDir = $this->componentRegistrar->getPath(
581  strtolower($libraryName)
582  );
583  }
584  } else {
585  $lib = strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $lib));
586  $libraryName = $vendor . '/' . $lib;
587  $libraryDir = $this->componentRegistrar->getPath(
589  strtolower($libraryName)
590  );
591  }
592  }
593  if ($libraryDir) {
594  $fullPath = $libraryDir . '/' . implode('/', $namespaceParts) . '/' .
595  str_replace('\\', '/', $badClass) . '.php';
596  }
597 
598  return $fullPath;
599  }
600 
607  private function removeSpecialCasesForAllOthers(string $namespacePath, string $badClass, array &$badClasses): bool
608  {
609  // Remove usage of classes that do NOT using fully-qualified class names (possibly under same namespace)
610  $directories = [
611  BP . '/dev/tools/',
612  BP . '/dev/tests/api-functional/framework/',
613  BP . '/dev/tests/functional/',
614  BP . '/dev/tests/integration/framework/',
615  BP . '/dev/tests/integration/framework/tests/unit/testsuite/',
616  BP . '/dev/tests/integration/testsuite/',
617  BP . '/dev/tests/integration/testsuite/Magento/Test/Integrity/',
618  BP . '/dev/tests/static/framework/',
619  BP . '/dev/tests/static/testsuite/',
620  BP . '/setup/src/',
621  ];
622  $libraryPaths = $this->componentRegistrar->getPaths(ComponentRegistrar::LIBRARY);
623  $directories = array_merge($directories, $libraryPaths);
624  // Full list of directories where there may be namespace classes
625  foreach ($directories as $directory) {
626  $fullPath = $directory . $namespacePath . '/' . str_replace('\\', '/', $badClass) . '.php';
627  if (file_exists($fullPath)) {
628  unset($badClasses[array_search($badClass, $badClasses)]);
629 
630  return true;
631  }
632  }
633 
634  return false;
635  }
636 
644  private function assertClassReferences(array $badClasses, string $file): void
645  {
646  if (empty($badClasses)) {
647  return;
648  }
649  $this->fail("Incorrect namespace usage(s) found in file {$file}:\n" . implode("\n", $badClasses));
650  }
651 
652  public function testCoversAnnotation()
653  {
654  $files = Files::init();
655  $errors = [];
656  $filesToTest = $files->getPhpFiles(Files::INCLUDE_TESTS);
657 
658  if (($key = array_search(str_replace('\\', '/', __FILE__), $filesToTest)) !== false) {
659  unset($filesToTest[$key]);
660  }
661 
662  foreach ($filesToTest as $file) {
663  $code = file_get_contents($file);
664  if (preg_match('/@covers(DefaultClass)?\s+([\w\\\\]+)(::([\w\\\\]+))?/', $code, $matches)) {
665  if ($this->isNonexistentEntityCovered($matches)) {
666  $errors[] = $file . ': ' . $matches[0];
667  }
668  }
669  }
670  if ($errors) {
671  $this->fail(
672  'Nonexistent classes/methods were found in @covers annotations: ' . PHP_EOL . implode(PHP_EOL, $errors)
673  );
674  }
675  }
676 
681  private function isNonexistentEntityCovered($matches)
682  {
683  return !empty($matches[2]) && !class_exists($matches[2])
684  || !empty($matches[4]) && !method_exists($matches[2], $matches[4]);
685  }
686 }
$contents
Definition: website.php:14
defined('TESTS_BP')||define('TESTS_BP' __DIR__
Definition: _bootstrap.php:60
defined('MTF_BOOT_FILE')||define('MTF_BOOT_FILE' __FILE__
Definition: bootstrap.php:7
static getCallbackClass($callbackName)
Definition: Classes.php:109
static collectClassesInConfig(\SimpleXMLElement $xml)
Definition: Classes.php:121
static getXmlAttributeValues(\SimpleXMLElement $xml, $xPath, $attributeName)
Definition: Classes.php:90
static collectLayoutClasses(\SimpleXMLElement $xml)
Definition: Classes.php:160
$_option $_optionId $class
Definition: date.phtml:13
static getAllMatches($contents, $regex, &$result=[])
Definition: Classes.php:29
const BP
Definition: autoload.php:14
static isAutogenerated($className)
Definition: Classes.php:272
$relativePath
Definition: get.php:35
static getXmlNodeValues(\SimpleXMLElement $xml, $xPath)
Definition: Classes.php:55
foreach($appDirs as $dir) $files
$errors
Definition: overview.phtml:9
$code
Definition: info.phtml:12
if($currentSelectedMethod==$_code) $className
Definition: form.phtml:31