Completed
Push — master ( 833af1...b53336 )
by Amine
10s
created

InteractiveCommand::setupSubCommands()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php namespace Tarsana\Command\Commands;
2
3
use Tarsana\Command\Helpers\SyntaxHelper;
4
use Tarsana\Command\SubCommand;
5
use Tarsana\Syntax\ArraySyntax;
6
use Tarsana\Syntax\Factory as S;
7
use Tarsana\Syntax\ObjectSyntax;
8
use Tarsana\Syntax\OptionalSyntax;
9
use Tarsana\Syntax\Syntax;
10
11
class InteractiveCommand extends SubCommand {
12
13
    const KEYS = [
14
        10  => 'enter',
15
        127 => 'backspace',
16
        65  => 'up',
17
        66  => 'down',
18
        67  => 'right',
19
        68  => 'left',
20
        9   => 'tab'
21
    ];
22
23
    protected $helper;
24
    protected $confirmSyntax;
25
26
    protected function init()
27
    {
28
        $this->name('Interactive')
29
             ->description('Reads the command arguments and options interactively.');
30
        $this->helper = SyntaxHelper::instance();
31
        $this->confirmSyntax = S::optional(S::boolean(), false);
32
    }
33
34
    protected function setupSubCommands()
35
    {
36
        return $this;
37
    }
38
39
    protected function execute()
40
    {
41
        $parent = $this->parent;
42
        $syntax = $parent->syntax();
43
        $this->console->out('<save>');
44
45
        if ($syntax) {
46
            $args = $this->read($syntax);
47
            $parent->args($args);
48
        }
49
50
        $options = array_keys($parent->options());
51
        $chosen = [];
52
        foreach($options as $option) {
53
            $bool = $this->read($this->confirmSyntax, $option, true);
54
            $parent->options[$option] = $bool;
55
            if ($bool) {
56
                $chosen[] = $option;
57
            }
58
        }
59
60
        $options = implode(' ', $chosen) . ' ';
61
        $args = $syntax ? $syntax->dump($args) : '';
0 ignored issues
show
Bug introduced by
The variable $args does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
62
63
        $this->console->out('<load><clearAfter>');
64
        $this->console->line("> {$options}{$args}<br>");
65
66
        return $this->parent->fire();
67
    }
68
69
    protected function read(Syntax $syntax, string $prefix = '', bool $display = false)
70
    {
71
        if ($display) {
72
            $this->display($syntax, $prefix);
73
        }
74
75
        $type = $this->helper->type($syntax);
76
        $result = null;
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
77
        switch ($type) {
78
            case 'object':
79
                $result = $this->readObject($syntax, $prefix);
0 ignored issues
show
Compatibility introduced by
$syntax of type object<Tarsana\Syntax\Syntax> is not a sub-type of object<Tarsana\Syntax\ObjectSyntax>. It seems like you assume a child class of the class Tarsana\Syntax\Syntax to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
80
            break;
81
            case 'array':
82
                $result = $this->readArray($syntax, $prefix);
0 ignored issues
show
Compatibility introduced by
$syntax of type object<Tarsana\Syntax\Syntax> is not a sub-type of object<Tarsana\Syntax\ArraySyntax>. It seems like you assume a child class of the class Tarsana\Syntax\Syntax to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
83
            break;
84
            case 'optional':
85
                $result = $this->readOptional($syntax, $prefix);
0 ignored issues
show
Compatibility introduced by
$syntax of type object<Tarsana\Syntax\Syntax> is not a sub-type of object<Tarsana\Syntax\OptionalSyntax>. It seems like you assume a child class of the class Tarsana\Syntax\Syntax to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
86
            break;
87
            default:
88
                $result = $this->readSimple($syntax);
89
            break;
90
        }
91
92
        return $result;
93
    }
94
95
    protected function display(Syntax $syntax, string $name)
96
    {
97
        $text = $this->helper->asString($syntax);
98
        $default = '';
99
        if ($syntax instanceof OptionalSyntax) {
100
            $default = '(default: ' . json_encode($syntax->getDefault()) . ')';
101
        }
102
        $description = $this->parent->describe($name);
103
        $this->console->out(
104
            "<success>{$name}</success> <warn>{$text}</warn>"
105
          . " {$description} <warn>{$default}</warn><br>"
106
        );
107
    }
108
109
    protected function readObject(ObjectSyntax $syntax, string $prefix)
110
    {
111
        $result = [];
112
        if ($prefix != '')
113
            $prefix .= '.';
114
        foreach ($syntax->fields() as $name => $s) {
0 ignored issues
show
Bug introduced by
The expression $syntax->fields() of type array|object<Tarsana\Syntax\ObjectSyntax> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
115
            $fullname = $prefix . $name;
116
            $result[$name] = $this->read($s, $fullname, true);
117
        }
118
        return (object) $result;
119
    }
120
121
    protected function readArray(ArraySyntax $syntax, string $prefix)
122
    {
123
        $result = [];
124
        $repeat = true;
125
        while ($repeat) {
126
            $result[] = $this->read($syntax->syntax(), $prefix);
127
            $this->console->out("Add new item to <success>{$prefix}</success>?<br>");
128
            $repeat = $this->readOptional($this->confirmSyntax, '');
129
            $this->clearLines(3);
130
        }
131
        return $result;
132
    }
133
134
    protected function readOptional(OptionalSyntax $syntax, string $prefix)
135
    {
136
        $default = $syntax->syntax()->dump($syntax->getDefault());
137
        $this->console->out("<color:252>{$default}<column:1><reset>");
138
        $n = ord($this->console->char());
139
        $this->console->out('<column:1><clearLine>');
140
        if (array_key_exists($n, static::KEYS) && static::KEYS[$n] == 'enter')
141
            return $syntax->getDefault();
142
        return $this->read($syntax->syntax(), $prefix);
143
    }
144
145
    protected function readSimple(Syntax $syntax)
146
    {
147
        $this->console->out('<column:1>> ');
148
        $done = false;
149
        $text = '';
150
        $result = null;
151
        while (! $done) {
152
            $c = $this->readChar();
153
            switch($c) {
154
                case 'enter':
155
                    $done = true;
156
                break;
157
                case 'backspace':
158
                    $text = substr($text, 0, -1);
159
                break;
160
                default:
161
                    $text .= $c;
162
                break;
163
            }
164
165
            try {
166
                $result = $syntax->parse($text);
167
                $this->clearLines(1);
168
                $this->console->out("> {$text}");
169
            } catch (\Exception $e) {
170
                $this->clearLines(1);
171
                $this->console->out("> <warn>{$text}</warn>");
172
                $done = false;
173
            }
174
        }
175
        $this->console->out('<br>');
176
177
        return $result;
178
    }
179
180
    protected function readChar() : string
181
    {
182
        $c = $this->console->char();
183
        if (ctype_print($c))
184
            return $c;
185
        $n = ord($c);
186
        if (
187
            array_key_exists($n, static::KEYS)
188
            && in_array(static::KEYS[$n], ['enter', 'backspace'])
189
        ) {
190
            return static::KEYS[$n];
191
        }
192
        return '';
193
    }
194
195
    protected function clearLines(int $number)
196
    {
197
        $text = '<clearLine>'
198
            . str_repeat('<prevLine><clearLine>', $number - 1)
199
            . '<column:1>';
200
        $this->console->out($text);
201
    }
202
}
203