Passed
Pull Request — 8.x-1.x (#16)
by Frédéric G.
05:02
created

Undeclared::moduleActualDependencies()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 11
c 1
b 0
f 1
dl 0
loc 18
rs 9.9
cc 4
nc 4
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Drupal\qa\Plugin\QaCheck\Dependencies;
6
7
use Drupal\qa\Data;
8
use Drupal\qa\Pass;
9
use Drupal\qa\Plugin\QaCheckBase;
10
use Drupal\qa\Plugin\QaCheckInterface;
11
use Drupal\qa\Plugin\QaCheckManager;
12
use Drupal\qa\Result;
13
use PhpParser\Error;
0 ignored issues
show
Bug introduced by
The type PhpParser\Error was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use PhpParser\NodeTraverser;
0 ignored issues
show
Bug introduced by
The type PhpParser\NodeTraverser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use PhpParser\ParserFactory;
0 ignored issues
show
Bug introduced by
The type PhpParser\ParserFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use Symfony\Component\DependencyInjection\ContainerInterface;
17
18
/**
19
 * Undeclared checks undeclared module dependencies based on function calls.
20
 *
21
 * It only covers:
22
 * - function calls (not method calls),
23
 * - in .module files,
24
 * - to functions located in other module files.
25
 *
26
 * Later versions could go further.
27
 *
28
 * @QaCheck(
29
 *   id = "dependencies.undeclared",
30
 *   label = @Translation("Undeclared dependencies"),
31
 *   details = @Translation("This check finds modules doing cross-module function calls to other modules not declared as dependencies."),
32
 *   usesBatch = false,
33
 *   steps = 1,
34
 * )
35
 */
36
class Undeclared extends QaCheckBase implements QaCheckInterface {
37
38
  const NAME = 'dependencies.undeclared';
39
40
  /**
41
   * The extension.list.modules service.
42
   *
43
   * @var \Drupal\Core\Extension\ExtensionList
44
   */
45
  protected $elm;
46
47
  /**
48
   * A PHP-Parser parser.
49
   *
50
   * @var \PhpParser\Parser
0 ignored issues
show
Bug introduced by
The type PhpParser\Parser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
51
   */
52
  protected $parser;
53
54
  /**
55
   * The plugin_manager.qa_check service.
56
   *
57
   * @var \Drupal\qa\Plugin\QaCheckManager
58
   */
59
  protected $qam;
60
61
  /**
62
   * Undeclared constructor.
63
   *
64
   * @param array $configuration
65
   *   The plugin configuration.
66
   * @param string $id
67
   *   The plugin ID.
68
   * @param array $definition
69
   *   The plugin definition.
70
   * @param \Drupal\Core\Extension\ModuleExtensionList $elm
71
   *   The extension.list.module service.
72
   * @param \Drupal\qa\Plugin\QaCheckManager $qam
73
   *   The plugin_manager.qa_check service.
74
   */
75
  public function __construct(
76
    array $configuration,
77
    string $id,
78
    array $definition,
79
    ModuleExtensionList $elm,
0 ignored issues
show
Bug introduced by
The type Drupal\qa\Plugin\QaCheck...ies\ModuleExtensionList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
80
    QaCheckManager $qam
81
  ) {
82
    parent::__construct($configuration, $id, $definition);
83
    $this->elm = $elm;
84
    $this->qam = $qam;
85
86
    $this->qam->initInternalFunctions();
87
88
    // For the sake of evolution, ignore PHP5-only code.
89
    $this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
90
  }
91
92
  /**
93
   * {@inheritdoc}
94
   */
95
  public static function create(
96
    ContainerInterface $container,
97
    array $configuration,
98
    $id,
99
    $definition
100
  ) {
101
    $elm = $container->get('extension.list.module');
102
    $qam = $container->get(Data::MANAGER);
103
    return new static($configuration, $id, $definition, $elm, $qam);
0 ignored issues
show
Bug introduced by
It seems like $elm can also be of type null; however, parameter $elm of Drupal\qa\Plugin\QaCheck...declared::__construct() does only seem to accept Drupal\qa\Plugin\QaCheck...ies\ModuleExtensionList, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
    return new static($configuration, $id, $definition, /** @scrutinizer ignore-type */ $elm, $qam);
Loading history...
Bug introduced by
It seems like $qam can also be of type null; however, parameter $qam of Drupal\qa\Plugin\QaCheck...declared::__construct() does only seem to accept Drupal\qa\Plugin\QaCheckManager, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
    return new static($configuration, $id, $definition, $elm, /** @scrutinizer ignore-type */ $qam);
Loading history...
104
  }
105
106
  /**
107
   * Build the list of module names regardless of module installation status.
108
   *
109
   * @return array
110
   *   A map of installed status by module name.
111
   */
112
  protected function getModulesToScan(): array {
113
    $list = $this->elm->getList();
114
    $list = array_filter($list, function ($ext) {
115
      $isCore = substr($ext->getPath(), 0, 5) === 'core/';
116
      return !$isCore;
117
    });
118
    $list = array_flip(array_keys($list));
119
    $installed = $this->elm->getAllInstalledInfo();
120
    foreach ($list as $module => &$on) {
121
      $on = isset($installed[$module]);
122
    }
123
    return $list;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $list could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
124
  }
125
126
  /**
127
   * Build the list of function calls in a single source file.
128
   *
129
   * @param string $path
130
   *   The absolute path to the module file.
131
   *
132
   * @return array
133
   *   An array of function names.
134
   */
135
  protected function functionCalls(string $path): array {
136
    $code = file_get_contents($path);
137
    try {
138
      $stmts = $this->parser->parse($code);
139
    }
140
    catch (Error $e) {
141
      echo "Skipping ${path} for parse error: " . $e->getMessage();
142
      return [];
143
    }
144
145
    $traverser = new NodeTraverser();
146
    $traverser->addVisitor($visitor = new FunctionCallVisitor());
147
    $traverser->traverse($stmts);
148
    $pad = array_unique($visitor->pad);
149
    // Ignore builtin/extension functions.
150
    $pad = array_filter($pad, function ($name) {
151
      return empty($this->internalFunctions[$name]);
0 ignored issues
show
Bug Best Practice introduced by
The property internalFunctions does not exist on Drupal\qa\Plugin\QaCheck\Dependencies\Undeclared. Did you maybe forget to declare it?
Loading history...
152
    });
153
    return $pad;
154
  }
155
156
  /**
157
   * Build the list of actual module dependencies in all modules based on calls.
158
   *
159
   * @param string $name
160
   *   The name of the module for which to list dependencies.
161
   * @param array $calls
162
   *   The function calls performed by that module.
163
   *
164
   * @return array
165
   *   A map of modules names by module name.
166
   */
167
  protected function moduleActualDependencies(string $name, array $calls): array {
168
    $modules = [];
169
    foreach ($calls as $called) {
170
      try {
171
        $rf = new \ReflectionFunction($called);
172
      }
173
      catch (\ReflectionException $e) {
174
        $modules[] = "(${called})";
175
        continue;
176
      }
177
178
      // Drupal name-based magic.
179
      $module = basename($rf->getFileName(), '.module');
180
      if ($module !== $name) {
181
        $modules[] = $module;
182
      }
183
    }
184
    return $modules;
185
  }
186
187
  /**
188
   * Build the list of module dependencies in all modules based on module info.
189
   *
190
   * @param string $name
191
   *   The name of the module for which to list dependencies.
192
   *
193
   * @return array
194
   *   A map of modules names by module name.
195
   */
196
  protected function moduleDeclaredDependencies(string $name): array {
197
    $info = $this->elm->getExtensionInfo($name);
198
    $deps = $info['dependencies'] ?? [];
199
    $res = [];
200
    foreach ($deps as $dep) {
201
      $ar = explode(":", $dep);
202
      $res[] = array_pop($ar);
203
    }
204
    return $res;
205
  }
206
207
  /**
208
   * Perform the undeclared calls check.
209
   *
210
   * @return \Drupal\qa\Result
211
   *   The result.
212
   */
213
  public function check(): Result {
214
    $modules = $this->getModulesToScan();
215
    $all = $this->elm->getList();
216
    $undeclared = [];
217
    foreach ($modules as $module => $installed) {
218
      $path = $all[$module]->getExtensionPathname();
219
      if (!$installed || empty($path)) {
220
        continue;
221
      }
222
      $calls = $this->functionCalls($path);
223
      $actual = $this->moduleActualDependencies($module, $calls);
224
      $declared = $this->moduleDeclaredDependencies($module);
225
      $missing = array_diff($actual, $declared);
226
      if (!empty($missing)) {
227
        $undeclared[$module] = $missing;
228
      }
229
    }
230
231
    return new Result('function_calls', empty($undeclared), $undeclared);
232
  }
233
234
  /**
235
   * {@inheritdoc}
236
   */
237
  public function run(): Pass {
238
    $pass = parent::run();
239
    $pass->record($this->check());
240
    $pass->life->end();
241
    return $pass;
242
  }
243
244
}
245