Commit 343ccdf8 authored by Gábor Hojtsy's avatar Gábor Hojtsy
Browse files

Issue #3111022 by Gábor Hojtsy, lauriii: Detect use of theme functions

parent 1ba55cea
......@@ -49,7 +49,7 @@ final class DeprecationAnalyzer {
/**
* Temporary directory to use for running phpstan.
*
*
* @var string
*/
protected $temporaryDirectory;
......@@ -82,6 +82,13 @@ final class DeprecationAnalyzer {
*/
protected $libraryDeprecationAnalyzer;
/**
* The theme function deprecation analyzer.
*
* @var \Drupal\upgrade_status\ThemeFunctionDeprecationAnalyzer
*/
protected $themeFunctionDeprecationAnalyzer;
/**
* Constructs a deprecation analyzer.
*
......@@ -97,6 +104,8 @@ final class DeprecationAnalyzer {
* The Twig environment.
* @param \Drupal\upgrade_status\LibraryDeprecationAnalyzer
* The library deprecation analyzer.
* @param \Drupal\upgrade_status\ThemeFunctionDeprecationAnalyzer
* The theme function deprecation analyzer.
*/
public function __construct(
KeyValueFactoryInterface $key_value_factory,
......@@ -104,7 +113,8 @@ final class DeprecationAnalyzer {
Client $http_client,
FileSystemInterface $file_system,
TwigEnvironment $twig_environment,
LibraryDeprecationAnalyzer $library_deprecation_analyzer
LibraryDeprecationAnalyzer $library_deprecation_analyzer,
ThemeFunctionDeprecationAnalyzer $theme_function_deprecation_analyzer
) {
$this->scanResultStorage = $key_value_factory->get('upgrade_status_scan_results');
// Log errors to an upgrade status logger channel.
......@@ -113,6 +123,7 @@ final class DeprecationAnalyzer {
$this->fileSystem = $file_system;
$this->twigEnvironment = $twig_environment;
$this->libraryDeprecationAnalyzer = $library_deprecation_analyzer;
$this->themeFunctionDeprecationAnalyzer = $theme_function_deprecation_analyzer;
$this->vendorPath = $this->findVendorPath();
......@@ -186,6 +197,16 @@ final class DeprecationAnalyzer {
$result['data']['totals']['file_errors']++;
}
$theme_function_deprecations = $this->themeFunctionDeprecationAnalyzer->analyze($extension);
foreach ($theme_function_deprecations as $deprecation_message) {
$result['data']['files'][$deprecation_message->getFile()]['messages'][] = [
'message' => $deprecation_message->getMessage(),
'line' => $deprecation_message->getLine(),
];
$result['data']['totals']['errors']++;
$result['data']['totals']['file_errors']++;
}
// Manually add on info file incompatibility to results.
$info = $extension->info;
if (!isset($info['core_version_requirement'])) {
......@@ -239,7 +260,7 @@ final class DeprecationAnalyzer {
foreach($errors['messages'] as &$error) {
// Overwrite message with processed text. Save category.
list($message, $category) = $this->categorizeMessage($error['message'], $extension);
[$message, $category] = $this->categorizeMessage($error['message'], $extension);
$error['message'] = $message;
$error['upgrade_status_category'] = $category;
......
<?php
declare(strict_types=1);
namespace Drupal\upgrade_status;
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\Container;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Theme\Registry;
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Function_;
/**
* A theme function deprecation analyzer.
*/
final class ThemeFunctionDeprecationAnalyzer {
/**
* The service container.
*
* @var \Drupal\Core\DependencyInjection\Container
*/
private $container;
/**
* Constructs a new theme function deprecation analyzer.
*
* @param \Drupal\Core\DependencyInjection\Container $this->container
* The service container.
*/
public function __construct(Container $container) {
$this->container = $container;
}
/**
* Analyzes theme functions in an extension.
*
* @param \Drupal\Core\Extension\Extension $extension
* The extension to be analyzed.
*
* @return \Drupal\upgrade_status\DeprecationMessage[]
*/
public function analyze(Extension $extension): array {
$deprecation_messages = [];
// Analyze hook_theme and hook_theme_registry_alter functions.
$deprecation_messages = array_merge($deprecation_messages, $this->analyzeFunction($extension->getName() . '_' . 'theme', $extension));
$deprecation_messages = array_merge($deprecation_messages, $this->analyzeFunction($extension->getName() . '_' . 'theme_registry_alter', $extension));
// If a theme is being analyzed, theme function overrides need to be
// analyzed.
if ($extension->getType() === 'theme') {
// Create new instance of theme registry to ensure that we have the most
// recent data without having to make changes to the production theme
// registry.
$theme_registry = new Registry($this->container->get('app.root'), new NullBackend('null'), $this->container->get('lock'), $this->container->get('module_handler'), $this->container->get('theme_handler'), $this->container->get('theme.initialization'), $extension->getName());
$theme_registry->setThemeManager($this->container->get('theme.manager'));
$theme_hooks = $theme_registry->get();
$theme_function_overrides = drupal_find_theme_functions($theme_hooks, [$extension->getName()]);
foreach ($theme_function_overrides as $machine_name => $theme_function_override) {
try {
$function = new \ReflectionFunction($extension->getName() . '_' . $machine_name);
$file = $function->getFileName();
$line = $function->getStartLine();
$deprecation_messages[$extension->getName() . '_' . $machine_name] = new DeprecationMessage(sprintf('The theme is overriding the "%s" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $machine_name), $file, $line);
} catch (\ReflectionException $e) {
// This should never happen because drupal_find_theme_functions()
// ensures that the function exists.
}
}
}
return $deprecation_messages;
}
/**
* Analyzes function for definition of theme functions.
*
* This doesn't recognize functions in all edge cases. For example, theme
* functions could be generated dynamically in a number of different ways.
* However, this will be useful in most use cases.
*
* @param $function
* The function to be analyzed.
* @param \Drupal\Core\Extension\Extension $extension
* The extension that is being tested.
*
* @return \Drupal\upgrade_status\DeprecationMessage[]
*/
private function analyzeFunction(string $function, Extension $extension): array {
$deprecation_messages = [];
try {
$function_reflection = new \ReflectionFunction($function);
} catch (\ReflectionException $e) {
// Not all extensions implement theme hooks.
return [];
}
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse(file_get_contents($function_reflection->getFileName()));
} catch (Error $error) {
// The function cannot be evaluated because of a syntax error.
$deprecation_messages[] = new DeprecationMessage(sprintf('Parse error while processing the %s hook implementation.', $theme_function), $function_reflection->getFileName(), $node->getStartLine());
}
if (!is_iterable($ast)) {
return [];
}
$finder = new NodeFinder();
// Find the node for the function that is being analyzed.
$function_node = $finder->findFirst($ast, function (Node $node) use ($function) {
return ($node instanceof Function_ && isset($node->name) && $node->name->name === $function);
});
if (!$function_node) {
// This should never happen because the file has been loaded based on the
// existence of the function.
return [];
}
// Find theme functions that have been defined using the array syntax.
// @code
// function hook_theme() {
// return [
// 'theme_hook' => ['function' => theme_function'],
// ];
// }
// @endcode
$theme_function_nodes = $finder->find([$function_node], function(Node $node) {
return ($node instanceof ArrayItem && $node->key instanceof String_ && $node->key->value === 'function');
});
foreach ($theme_function_nodes as $node) {
$theme_function = $node->value instanceof String_ ? sprintf('"%s"', $node->value->value) : 'an unknown';
$deprecation_messages[] = new DeprecationMessage(sprintf('The %s is defining %s theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $extension->getType(), $theme_function), $function_reflection->getFileName(), $node->getStartLine());
}
// Find theme functions that are being added to an existing array using
// the array square bracket syntax.
// @code
// function hook_theme_registry_alter(&$theme_registry) {
// $theme_registry['theme_hook']['function'] = 'another_theme_function';
// }
// @endcode
$theme_function_dim_nodes = $finder->find([$function_node], function(Node $node) {
return $node instanceof Assign && $node->var instanceof ArrayDimFetch && $node->var->dim instanceof String_ && $node->var->dim->value === 'function';
});
foreach ($theme_function_dim_nodes as $node) {
$theme_function = $node->expr instanceof String_ ? sprintf('"%s"', $node->expr->value) : 'an unknown';
$deprecation_messages[] = new DeprecationMessage(sprintf('The %s is defining %s theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $extension->getType(), $theme_function), $function_reflection->getFileName(), $node->getStartLine());
}
return $deprecation_messages;
}
}
name: 'Upgrade Status Test module with theme functions'
type: module
description: 'Test module for testing theme function deprecation messages'
core: 8.x
core_version_requirement: ^8 || ^9
package: Testing
version: VERSION
<?php
/**
* Implements hook_theme().
*/
function upgrade_status_test_theme_functions_theme() {
return [
'upgrade_status_test_theme_function' => [
'function' => 'upgrade_status_test_theme_function'
],
'upgrade_status_test_theme_function_another_function' => [],
'upgrade_status_test_theme_function_theme_function_override' => [],
];
}
/**
* Implements hook_theme_registry_alter().
*/
function upgrade_status_test_theme_functions_theme_registry_alter(&$theme_registry) {
$theme_registry['upgrade_status_test_theme_function_another_function']['function'] = 'upgrade_status_test_theme_function';
$theme_registry['upgrade_status_test_theme_function_non_existing_function']['function'] = sprintf('upgrade_status_test_theme_function');
}
/**
* Theme function used for testing.
*/
function upgrade_status_test_theme_function() {
return 'kitten';
}
......@@ -77,8 +77,8 @@ class UpgradeStatusAnalyzeTest extends UpgradeStatusTestBase {
$project = $key_value->get('upgrade_status_test_theme');
$this->assertNotEmpty($project);
$report = json_decode($project, TRUE);
$this->assertEquals(3, $report['data']['totals']['file_errors']);
$this->assertCount(2, $report['data']['files']);
$this->assertEquals(4, $report['data']['totals']['file_errors']);
$this->assertCount(3, $report['data']['files']);
$file = reset($report['data']['files']);
$message = $file['messages'][0];
$this->assertContains('Twig Tag "raw" is deprecated since version 1.21. Use "verbatim" instead in', $message['message']);
......@@ -88,6 +88,22 @@ class UpgradeStatusAnalyzeTest extends UpgradeStatusTestBase {
$this->assertEquals(0, $file['messages'][0]['line']);
$this->assertEquals('Theme is extending a deprecated library. The "upgrade_status_test_twig/deprecated_library" asset library is deprecated for testing.', $file['messages'][1]['message']);
$this->assertEquals(0, $file['messages'][1]['line']);
$file = next($report['data']['files']);
$this->assertEquals('The theme is overriding the "upgrade_status_test_theme_function_theme_function_override" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][0]['message']);
$this->assertEquals(6, $file['messages'][0]['line']);
$project = $key_value->get('upgrade_status_test_theme_functions');
$this->assertNotEmpty($project);
$report = json_decode($project, TRUE);
$this->assertEquals(3, $report['data']['totals']['file_errors']);
$this->assertCount(1, $report['data']['files']);
$file = reset($report['data']['files']);
$this->assertEquals('The module is defining "upgrade_status_test_theme_function" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][0]['message']);
$this->assertEquals(9, $file['messages'][0]['line']);
$this->assertEquals('The module is defining "upgrade_status_test_theme_function" theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][1]['message']);
$this->assertEquals(20, $file['messages'][1]['line']);
$this->assertEquals('The module is defining an unknown theme function. Theme functions are deprecated. For more info, see https://www.drupal.org/node/2575445.', $file['messages'][2]['message']);
$this->assertEquals(21, $file['messages'][2]['line']);
$project = $key_value->get('upgrade_status_test_library');
$this->assertNotEmpty($project);
......
......@@ -26,6 +26,7 @@ abstract class UpgradeStatusTestBase extends BrowserTestBase {
'upgrade_status_test_submodules_a',
'upgrade_status_test_contrib_error',
'upgrade_status_test_contrib_no_error',
'upgrade_status_test_theme_functions',
'upgrade_status_test_twig',
'upgrade_status_test_library',
];
......@@ -50,6 +51,7 @@ abstract class UpgradeStatusTestBase extends BrowserTestBase {
'custom[data][data][upgrade_status_test_submodules]' => TRUE,
'custom[data][data][upgrade_status_test_twig]' => TRUE,
'custom[data][data][upgrade_status_test_theme]' => TRUE,
'custom[data][data][upgrade_status_test_theme_functions]' => TRUE,
'custom[data][data][upgrade_status_test_library]' => TRUE,
'contrib[data][data][upgrade_status_test_contrib_error]' => TRUE,
'contrib[data][data][upgrade_status_test_contrib_no_error]' => TRUE,
......
......@@ -132,7 +132,7 @@ class UpgradeStatusUiTest extends UpgradeStatusTestBase {
$this->drupalGet(Url::fromRoute('upgrade_status.report'));
$page = $this->getSession()->getPage();
$this->assertCount(6, $page->findAll('css', '.upgrade-status-summary-custom tr[class*=\'project-\']'));
$this->assertCount(7, $page->findAll('css', '.upgrade-status-summary-custom tr[class*=\'project-\']'));
$this->assertCount(4, $page->findAll('css', '.upgrade-status-summary-contrib tr[class*=\'project-\']'));
}
......
<?php
/**
* Theme function override for testing purposes.
*/
function upgrade_status_test_theme_upgrade_status_test_theme_function_theme_function_override() {
}
services:
upgrade_status.deprecation_analyzer:
class: Drupal\upgrade_status\DeprecationAnalyzer
arguments: ['@keyvalue', '@logger.channel.upgrade_status', '@http_client', '@file_system', '@twig', '@upgrade_status.library_deprecation_analyzer']
arguments: ['@keyvalue', '@logger.channel.upgrade_status', '@http_client', '@file_system', '@twig', '@upgrade_status.library_deprecation_analyzer', '@upgrade_status.theme_function_deprecation_analyzer']
upgrade_status.library_deprecation_analyzer:
class: Drupal\upgrade_status\LibraryDeprecationAnalyzer
arguments: ['@library.discovery.parser', '@twig', '@extension.list.module', '@extension.list.theme', '@extension.list.profile']
upgrade_status.theme_function_deprecation_analyzer:
class: Drupal\upgrade_status\ThemeFunctionDeprecationAnalyzer
arguments: ['@service_container']
upgrade_status.project_collector:
class: Drupal\upgrade_status\ProjectCollector
arguments: ['@extension.list.module', '@extension.list.theme', '@extension.list.profile']
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment