Completed
Pull Request — master (#75)
by
unknown
15:53 queued 09:22
created

CompletionHandler   C

Complexity

Total Complexity 73

Size/Duplication

Total Lines 441
Duplicated Lines 11.11 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 73
c 2
b 0
f 0
lcom 1
cbo 9
dl 49
loc 441
rs 5.5447

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
A setContext() 0 4 1
A getContext() 0 4 1
B runCompletion() 0 34 5
A completeForOptions() 0 16 3
B completeForOptionShortcuts() 0 14 5
C completeForOptionShortcutValues() 25 25 8
C completeForOptionValues() 24 24 7
A addHandlers() 0 4 1
A addHandler() 0 4 1
A getInput() 0 9 1
A completeForCommandName() 0 8 4
C completeForCommandArguments() 0 28 8
B getCompletionHelper() 0 16 7
A completeOption() 0 12 3
C mapArgumentsToWords() 0 31 7
A getOptionWordsWithValues() 0 16 4
A filterResults() 0 8 1
A getAllOptions() 0 11 2
A getCommandNames() 0 14 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CompletionHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CompletionHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Stecman\Component\Symfony\Console\BashCompletion;
4
5
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
6
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
7
use Symfony\Component\Console\Application;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\ArrayInput;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputOption;
12
13
class CompletionHandler
14
{
15
    /**
16
     * Application to complete for
17
     * @var \Symfony\Component\Console\Application
18
     */
19
    protected $application;
20
21
    /**
22
     * @var Command
23
     */
24
    protected $command;
25
26
    /**
27
     * @var CompletionContext
28
     */
29
    protected $context;
30
31
    /**
32
     * Array of completion helpers.
33
     * @var CompletionInterface[]
34
     */
35
    protected $helpers = array();
36
37
    public function __construct(Application $application, CompletionContext $context = null)
38
    {
39
        $this->application = $application;
40
        $this->context = $context;
41
42
        $this->addHandler(
43
            new Completion(
44
                'help',
45
                'command_name',
46
                Completion::TYPE_ARGUMENT,
47
                $this->getCommandNames()
48
            )
49
        );
50
51
        $this->addHandler(
52
            new Completion(
53
                'list',
54
                'namespace',
55
                Completion::TYPE_ARGUMENT,
56
                $application->getNamespaces()
57
            )
58
        );
59
    }
60
61
    public function setContext(CompletionContext $context)
62
    {
63
        $this->context = $context;
64
    }
65
66
    /**
67
     * @return CompletionContext
68
     */
69
    public function getContext()
70
    {
71
        return $this->context;
72
    }
73
74
    /**
75
     * @param CompletionInterface[] $array
76
     */
77
    public function addHandlers(array $array)
78
    {
79
        $this->helpers = array_merge($this->helpers, $array);
80
    }
81
82
    /**
83
     * @param CompletionInterface $helper
84
     */
85
    public function addHandler(CompletionInterface $helper)
86
    {
87
        $this->helpers[] = $helper;
88
    }
89
90
    /**
91
     * Do the actual completion, returning an array of strings to provide to the parent shell's completion system
92
     *
93
     * @throws \RuntimeException
94
     * @return string[]
95
     */
96
    public function runCompletion()
97
    {
98
        if (!$this->context) {
99
            throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
100
        }
101
102
        $cmdName = $this->getInput()->getFirstArgument();
103
104
        try {
105
            $this->command = $this->application->find($cmdName);
106
        } catch (\InvalidArgumentException $e) {
107
            // Exception thrown, when multiple or none commands are found.
108
        }
109
110
        $process = array(
111
            'completeForOptionValues',
112
            'completeForOptionShortcuts',
113
            'completeForOptionShortcutValues',
114
            'completeForOptions',
115
            'completeForCommandName',
116
            'completeForCommandArguments'
117
        );
118
119
        foreach ($process as $methodName) {
120
            $result = $this->{$methodName}();
121
122
            if (false !== $result) {
123
                // Return the result of the first completion mode that matches
124
                return $this->filterResults((array) $result);
125
            }
126
        }
127
128
        return array();
129
    }
130
131
    /**
132
     * Get an InputInterface representation of the completion context
133
     *
134
     * @return ArrayInput
135
     */
136
    public function getInput()
137
    {
138
        // Filter the command line content to suit ArrayInput
139
        $words = $this->context->getWords();
140
        array_shift($words);
141
        $words = array_filter($words);
142
143
        return new ArrayInput($words);
144
    }
145
146
    /**
147
     * Attempt to complete the current word as a long-form option (--my-option)
148
     *
149
     * @return array|false
150
     */
151
    protected function completeForOptions()
152
    {
153
        $word = $this->context->getCurrentWord();
154
155
        if (substr($word, 0, 2) === '--') {
156
            $options = array();
157
158
            foreach ($this->getAllOptions() as $opt) {
159
                $options[] = '--'.$opt->getName();
160
            }
161
162
            return $options;
163
        }
164
165
        return false;
166
    }
167
168
    /**
169
     * Attempt to complete the current word as an option shortcut.
170
     *
171
     * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion.
172
     *
173
     * @return array|false
174
     */
175
    protected function completeForOptionShortcuts()
176
    {
177
        $word = $this->context->getCurrentWord();
178
179
        if (strpos($word, '-') === 0 && strlen($word) == 2) {
180
            $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition();
181
182
            if ($definition->hasShortcut(substr($word, 1))) {
183
                return array($word);
184
            }
185
        }
186
187
        return false;
188
    }
189
190
    /**
191
     * Attempt to complete the current word as the value of an option shortcut
192
     *
193
     * @return array|false
194
     */
195 View Code Duplication
    protected function completeForOptionShortcutValues()
196
    {
197
        $wordIndex = $this->context->getWordIndex();
198
199
        if ($this->command && $wordIndex > 1) {
200
            $left = $this->context->getWordAtIndex($wordIndex - 1);
201
202
            // Complete short options
203
            if ($left[0] == '-' && strlen($left) == 2) {
204
                $shortcut = substr($left, 1);
205
                $def = $this->command->getNativeDefinition();
206
207
                if (!$def->hasShortcut($shortcut)) {
208
                    return false;
209
                }
210
211
                $opt = $def->getOptionForShortcut($shortcut);
212
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
213
                    return $this->completeOption($opt);
214
                }
215
            }
216
        }
217
218
        return false;
219
    }
220
221
    /**
222
     * Attemp to complete the current word as the value of a long-form option
223
     *
224
     * @return array|false
225
     */
226 View Code Duplication
    protected function completeForOptionValues()
227
    {
228
        $wordIndex = $this->context->getWordIndex();
229
230
        if ($this->command && $wordIndex > 1) {
231
            $left = $this->context->getWordAtIndex($wordIndex - 1);
232
233
            if (strpos($left, '--') === 0) {
234
                $name = substr($left, 2);
235
                $def = $this->command->getNativeDefinition();
236
237
                if (!$def->hasOption($name)) {
238
                    return false;
239
                }
240
241
                $opt = $def->getOption($name);
242
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
243
                    return $this->completeOption($opt);
244
                }
245
            }
246
        }
247
248
        return false;
249
    }
250
251
    /**
252
     * Attempt to complete the current word as a command name
253
     *
254
     * @return array|false
255
     */
256
    protected function completeForCommandName()
257
    {
258
        if (!$this->command || (count($this->context->getWords()) == 2 && $this->context->getWordIndex() == 1)) {
259
            return $this->getCommandNames();
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * Attempt to complete the current word as a command argument value
267
     *
268
     * @see Symfony\Component\Console\Input\InputArgument
269
     * @return array|false
270
     */
271
    protected function completeForCommandArguments()
272
    {
273
        if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) {
274
            return false;
275
        }
276
277
        $definition = $this->command->getNativeDefinition();
278
        $argWords = $this->mapArgumentsToWords($definition->getArguments());
279
        $wordIndex = $this->context->getWordIndex();
280
281
        if (isset($argWords[$wordIndex])) {
282
            $name = $argWords[$wordIndex];
283
        } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) {
284
            $name = end($argWords);
285
        } else {
286
            return false;
287
        }
288
289
        if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) {
290
            return $helper->run();
291
        }
292
293
        if ($this->command instanceof CompletionAwareInterface) {
294
            return $this->command->completeArgumentValues($name, $this->context);
295
        }
296
297
        return false;
298
    }
299
300
    /**
301
     * Find a CompletionInterface that matches the current command, target name, and target type
302
     *
303
     * @param string $name
304
     * @param string $type
305
     * @return CompletionInterface|null
306
     */
307
    protected function getCompletionHelper($name, $type)
308
    {
309
        foreach ($this->helpers as $helper) {
310
            if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) {
311
                continue;
312
            }
313
314
            if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) {
315
                if ($helper->getTargetName() == $name) {
316
                    return $helper;
317
                }
318
            }
319
        }
320
321
        return null;
322
    }
323
324
    /**
325
     * Complete the value for the given option if a value completion is availble
326
     *
327
     * @param InputOption $option
328
     * @return array|false
329
     */
330
    protected function completeOption(InputOption $option)
331
    {
332
        if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) {
333
            return $helper->run();
334
        }
335
336
        if ($this->command instanceof CompletionAwareInterface) {
337
            return $this->command->completeOptionValues($option->getName(), $this->context);
338
        }
339
340
        return false;
341
    }
342
343
    /**
344
     * Step through the command line to determine which word positions represent which argument values
345
     *
346
     * The word indexes of argument values are found by eliminating words that are known to not be arguments (options,
347
     * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value,
348
     *
349
     * @param InputArgument[] $argumentDefinitions
350
     * @return array as [argument name => word index on command line]
351
     */
352
    protected function mapArgumentsToWords($argumentDefinitions)
353
    {
354
        $argumentPositions = array();
355
        $argumentNumber = 0;
356
        $previousWord = null;
357
        $argumentNames = array_keys($argumentDefinitions);
358
359
        // Build a list of option values to filter out
360
        $optionsWithArgs = $this->getOptionWordsWithValues();
361
362
        foreach ($this->context->getWords() as $wordIndex => $word) {
363
            // Skip program name, command name, options, and option values
364
            if ($wordIndex < 2
365
                || ($word && '-' === $word[0])
366
                || in_array($previousWord, $optionsWithArgs)) {
367
                $previousWord = $word;
368
                continue;
369
            } else {
370
                $previousWord = $word;
371
            }
372
373
            // If argument n exists, pair that argument's name with the current word
374
            if (isset($argumentNames[$argumentNumber])) {
375
                $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber];
376
            }
377
378
            $argumentNumber++;
379
        }
380
381
        return $argumentPositions;
382
    }
383
384
    /**
385
     * Build a list of option words/flags that will have a value after them
386
     * Options are returned in the format they appear as on the command line.
387
     *
388
     * @return string[] - eg. ['--myoption', '-m', ... ]
389
     */
390
    protected function getOptionWordsWithValues()
391
    {
392
        $strings = array();
393
394
        foreach ($this->getAllOptions() as $option) {
395
            if ($option->isValueRequired()) {
396
                $strings[] = '--' . $option->getName();
397
398
                if ($option->getShortcut()) {
399
                    $strings[] = '-' . $option->getShortcut();
400
                }
401
            }
402
        }
403
404
        return $strings;
405
    }
406
407
    /**
408
     * Filter out results that don't match the current word on the command line
409
     *
410
     * @param string[] $array
411
     * @return string[]
412
     */
413
    protected function filterResults(array $array)
414
    {
415
        $curWord = $this->context->getCurrentWord();
416
417
        return array_filter($array, function($val) use ($curWord) {
418
            return fnmatch($curWord.'*', $val);
419
        });
420
    }
421
422
    /**
423
     * Get the combined options of the application and entered command
424
     *
425
     * @return InputOption[]
426
     */
427
    protected function getAllOptions()
428
    {
429
        if (!$this->command) {
430
            return $this->application->getDefinition()->getOptions();
431
        }
432
433
        return array_merge(
434
            $this->command->getNativeDefinition()->getOptions(),
435
            $this->application->getDefinition()->getOptions()
436
        );
437
    }
438
439
    protected function getCommandNames()
440
    {
441
        $commands = array();
442
443
        foreach ($this->application->all() as $name => $command) {
444
            if ($command->isHidden()) {
445
                continue;
446
            }
447
448
            $commands[] = $name;
449
        }
450
451
        return $commands;
452
    }
453
}
454