3 namespace Drupal\drupalmoduleupgrader;
5 use Drupal\Component\Serialization\Yaml;
6 use Drupal\drupalmoduleupgrader\Utility\Filter\ContainsLogicFilter;
7 use Drupal\drupalmoduleupgrader\Utility\Filter\FunctionCallArgumentFilter;
8 use Pharborist\DocCommentNode;
10 use Pharborist\Functions\FunctionCallNode;
11 use Pharborist\Functions\FunctionDeclarationNode;
12 use Pharborist\Functions\ParameterNode;
13 use Pharborist\LineCommentBlockNode;
14 use Pharborist\Objects\ClassNode;
15 use Pharborist\Parser;
16 use Pharborist\Variables\VariableNode;
17 use Pharborist\WhitespaceNode;
18 use Symfony\Component\Filesystem\Filesystem;
21 * Base class for converters.
23 abstract class ConverterBase extends PluginBase implements ConverterInterface {
25 // Used by buildFixMe() to determine the comment style of the generated
27 const LINE_COMMENT = '//';
28 const DOC_COMMENT = '/**/';
33 public function isExecutable(TargetInterface $target) {
34 // If the plugin applies to particular hook(s), only return TRUE if the
35 // target module implements any of the hooks. Otherwise, return TRUE
37 if (isset($this->pluginDefinition['hook'])) {
38 return (boolean) array_filter((array) $this->pluginDefinition['hook'], [ $target->getIndexer('function'), 'has' ]);
46 * Executes the target module's implementation of the specified hook, and
51 * @throws \LogicException if the target module doesn't implement the
52 * specified hook, or if the implementation contains logic.
56 protected function executeHook(TargetInterface $target, $hook) {
57 $indexer = $target->getIndexer('function');
59 if ($indexer->has($hook)) {
60 // Configure the ContainsLogicFilter so that certain "safe" functions
62 $has_logic = new ContainsLogicFilter();
63 $has_logic->whitelist('t');
64 $has_logic->whitelist('drupal_get_path');
66 $function = $indexer->get($hook);
67 if ($function->is($has_logic)) {
68 throw new \LogicException('{target}_{hook} cannot be executed because it contains logic.');
71 $function_name = $function->getName()->getText();
72 if (! function_exists($function_name)) {
73 eval($function->getText());
75 return call_user_func($function_name);
79 throw new \LogicException('{target} does not implement hook_{hook}.');
84 * Creates an empty implementation of a hook.
86 * @param TargetInterface $target
89 * The hook to implement, without the hook_ prefix.
91 * @return \Pharborist\Functions\FunctionDeclarationNode
92 * The hook implementation, appended to the main module file.
94 protected function implement(TargetInterface $target, $hook) {
95 $function = FunctionDeclarationNode::create($target->id() . '_' . $hook);
96 $function->setDocComment(DocCommentNode::create('Implements hook_' . $hook . '().'));
98 $module_file = $target->getPath('.module');
99 $target->open($module_file)->append($function);
101 WhitespaceNode::create("\n")->insertBefore($function);
102 WhitespaceNode::create("\n")->insertAfter($function);
108 * Writes a file to the target module's directory.
110 * @param TargetInterface $target
112 * @param string $path
113 * The path of the file to write, relative to the module root.
114 * @param string $data
118 * The path of the file, including the target's base path.
120 public function write(TargetInterface $target, $path, $data) {
123 $fs = new Filesystem();
126 $destination_path = $target->getPath($path);
127 $fs->dumpFile($destination_path, (string) $data);
129 return $destination_path;
133 * Writes a class to the target module's PSR-4 root.
135 * @param TargetInterface $target
137 * @param ClassNode $class
138 * The class to write. The path will be determined from the class'
139 * fully qualified name.
142 * The generated path to the class.
144 public function writeClass(TargetInterface $target, ClassNode $class) {
145 $class_path = ltrim($class->getName()->getAbsolutePath(), '\\');
146 $path = str_replace([ 'Drupal\\' . $target->id(), '\\', ], [ 'src', '/' ], $class_path) . '.php';
148 return $this->write($target, $path, $class->parents()->get(0));
152 * Writes out arbitrary data in YAML format.
154 * @param TargetInterface $target
156 * @param string $group
157 * The name of the YAML file. It will be prefixed with the module's machine
158 * name and suffixed with .yml. For example, a group value of 'routing'
159 * will write MODULE.routing.yml.
163 * @todo This should be writeYAML, not writeInfo.
165 protected function writeInfo(TargetInterface $target, $group, array $data) {
166 $destination = $target->getPath('.' . $group . '.yml');
167 file_put_contents($destination, Yaml::encode($data));
171 * Writes a service definition to the target module's services.yml file.
173 * @param TargetInterface $target
175 * @param string $service_id
176 * The service ID. If an existing one with the same ID already exists,
177 * it will be overwritten.
178 * @param array $service_definition
180 protected function writeService(TargetInterface $target, $service_id, array $service_definition) {
181 $services = $target->getServices();
182 $services->set($service_id, $service_definition);
183 $this->writeInfo($target, 'services', [ 'services' => $services->toArray() ]);
187 * Parses a generated class into a syntax tree.
189 * @param string|array $class
190 * The class to parse, either as a string of PHP code or a renderable array.
192 * @return \Pharborist\Objects\ClassNode
194 protected function parse($class) {
195 if (is_array($class)) {
196 $class = \Drupal::service('renderer')->renderPlain($class);
198 return Parser::parseSnippet($class)->find(Filter::isInstanceOf('Pharborist\Objects\ClassNode'))[0];
202 * Builds a FIXME notice using either the text in the plugin definition,
205 * @param string|NULL $text
206 * The FIXME notice's text, with variable placeholders and no translation.
207 * @param array $variables
208 * Optional variables to use in translation. If empty, the FIXME will not
210 * @param string|NULL $style
211 * The comment style. Returns a LineCommentBlockNode if this is set to
212 * self::LINE_COMMENT, a DocCommentNode if self::DOC_COMMENT, or the FIXME
213 * as a string if set to anything else.
217 protected function buildFixMe($text = NULL, array $variables = [], $style = self::LINE_COMMENT) {
218 $fixMe = "@FIXME\n" . ($text ?: $this->pluginDefinition['fixme']);
220 if (isset($this->pluginDefinition['documentation'])) {
222 foreach ($this->pluginDefinition['documentation'] as $doc) {
224 $fixMe .= (isset($doc['url']) ? $doc['url'] : (string) $doc);
229 $fixMe = $this->t($fixMe, $variables);
233 case self::LINE_COMMENT:
234 return LineCommentBlockNode::create($fixMe);
236 case self::DOC_COMMENT:
237 return DocCommentNode::create($fixMe);
245 * Parametrically rewrites a function.
247 * @param \Drupal\drupalmoduleupgrader\RewriterInterface $rewriter
248 * A fully configured parametric rewriter.
249 * @param \Pharborist\Functions\ParameterNode $parameter
250 * The parameter upon which to base the rewrite.
251 * @param TargetInterface $target
253 * @param boolean $recursive
254 * If TRUE, rewriting will recurse into called functions which are passed
255 * the rewritten parameter as an argument.
257 protected function rewriteFunction(RewriterInterface $rewriter, ParameterNode $parameter, TargetInterface $target, $recursive = TRUE) {
258 $rewriter->rewrite($parameter);
259 $target->save($parameter);
261 // Find function calls within the rewritten function which are called
262 // with the rewritten parameter.
263 $indexer = $target->getIndexer('function');
266 ->find(new FunctionCallArgumentFilter($parameter->getName()))
267 ->filter(function(FunctionCallNode $call) use ($indexer) {
268 return $indexer->has($call->getName()->getText());
271 /** @var \Pharborist\Functions\FunctionCallNode $call */
272 foreach ($next as $call) {
273 /** @var \Pharborist\Functions\FunctionDeclarationNode $function */
274 $function = $indexer->get($call->getName()->getText());
276 foreach ($call->getArguments() as $index => $argument) {
277 if ($argument instanceof VariableNode && $argument->getName() == $parameter->getName()) {
278 $this->rewriteFunction($rewriter, $function->getParameterAtIndex($index), $target, $recursive);