From 7e948419cddac6b146ceaaeeac9d42f61b3a7627 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 13:37:28 +0100 Subject: [PATCH 01/13] Fix array_key_exists negation --- src/Psalm/Type/Reconciler.php | 5 ++++- tests/TypeReconciliation/ArrayKeyExistsTest.php | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1ae92b40571..7863d05c265 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -20,6 +20,7 @@ use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; use Psalm\Storage\Assertion; +use Psalm\Storage\Assertion\ArrayKeyDoesNotExist; use Psalm\Storage\Assertion\ArrayKeyExists; use Psalm\Storage\Assertion\Empty_; use Psalm\Storage\Assertion\Falsy; @@ -197,7 +198,9 @@ public static function reconcileKeyedTypes( $is_equality = $is_equality && $new_type_part_part instanceof IsIdentical; - $has_inverted_isset = $has_inverted_isset || $new_type_part_part instanceof IsNotIsset; + $has_inverted_isset = $has_inverted_isset + || $new_type_part_part instanceof IsNotIsset + || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check || $new_type_part_part instanceof NonEmptyCountable; diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 3ee780251c1..c8b8dafbaf8 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -46,6 +46,15 @@ function three(array $a): void { echo $a["a"]; echo $a["b"]; }', + ], + 'arrayKeyExistsNegation' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:17 +0100 Subject: [PATCH 02/13] Fix --- .../Statements/Block/IfElse/IfAnalyzer.php | 14 -------------- tests/TypeReconciliation/ArrayKeyExistsTest.php | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index ad95cce30d5..c74da5d3efe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -272,20 +272,6 @@ public static function analyze( array_keys($if_scope->negated_types), ); - $extra_vars_to_update = []; - - // if there's an object-like array in there, we also need to update the root array variable - foreach ($vars_to_update as $var_id) { - $bracked_pos = strpos($var_id, '['); - if ($bracked_pos !== false) { - $extra_vars_to_update[] = substr($var_id, 0, $bracked_pos); - } - } - - if ($extra_vars_to_update) { - $vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update)); - } - $outer_context->update( $old_if_context, $if_context, diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index c8b8dafbaf8..19174d347cd 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -56,6 +56,19 @@ function getMethodName(array $data = []): void { } ', ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ' + ] 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:24 +0100 Subject: [PATCH 03/13] Fix --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 19174d347cd..1390caeee13 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -68,7 +68,7 @@ function getMethodName(array $ddata = []): void { /** @psalm-check-type-exact $ddata = array */ } ' - ] + ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:07:55 +0100 Subject: [PATCH 04/13] cs-fix --- .../Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php | 3 --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index c74da5d3efe..2a2be74606f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -34,14 +34,11 @@ use function array_keys; use function array_merge; use function array_reduce; -use function array_unique; use function count; use function in_array; use function preg_match; use function preg_quote; use function spl_object_id; -use function strpos; -use function substr; /** * @internal diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 1390caeee13..e44bfa55464 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -67,7 +67,7 @@ function getMethodName(array $ddata = []): void { } /** @psalm-check-type-exact $ddata = array */ } - ' + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 16:03:08 +0100 Subject: [PATCH 05/13] Create keyed arrays when assigning literal union keys --- .../Assignment/ArrayAssignmentAnalyzer.php | 38 ++++++++----------- tests/ArrayAssignmentTest.php | 21 ++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index f00d81a48dd..576fe45a011 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -349,29 +349,23 @@ private static function updateTypeWithKeyValues( } if (!$has_matching_objectlike_property && !$has_matching_string) { - if (count($key_values) === 1) { - $key_value = $key_values[0]; - - $object_like = new TKeyedArray( - [$key_value->value => $current_type], - $key_value instanceof TLiteralClassString - ? [$key_value->value => true] - : null, - ); - - $array_assignment_type = new Union([ - $object_like, - ]); - } else { - $array_assignment_literals = $key_values; - - $array_assignment_type = new Union([ - new TNonEmptyArray([ - new Union($array_assignment_literals), - $current_type, - ]), - ]); + $properties = []; + $classStrings = []; + $current_type = $current_type->setPossiblyUndefined(count($key_values) > 1); + foreach ($key_values as $key_value) { + $properties[$key_value->value] = $current_type; + if ($key_value instanceof TLiteralClassString) { + $classStrings[$key_value->value] = true; + } } + $object_like = new TKeyedArray( + $properties, + $classStrings ?: null, + ); + + $array_assignment_type = new Union([ + $object_like, + ]); return Type::combineUnionTypes( $child_stmt_type, diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 38f98d70d60..4377c1e72cf 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -34,6 +34,27 @@ public function testConditionalAssignment(): void public function providerValidCodeParse(): iterable { return [ + 'assignUnionOfLiterals' => [ + 'code' => ' [ + '$result===' => 'array{a: true, b: true}', + '$resultOpt===' => 'array{a?: true, b?: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Fri, 1 Dec 2023 16:11:05 +0100 Subject: [PATCH 06/13] Fix --- tests/ArrayAssignmentTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 4377c1e72cf..364b6def325 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -213,7 +213,7 @@ class B {} 'assertions' => [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', - '$bat' => 'non-empty-array', + '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ @@ -1000,6 +1000,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } From 1a4656564a5b3c41d5444473d31725936db86b6a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 16:31:01 +0100 Subject: [PATCH 07/13] Cleanup --- tests/TypeReconciliation/TypeAlgebraTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TypeReconciliation/TypeAlgebraTest.php b/tests/TypeReconciliation/TypeAlgebraTest.php index aea50a778e6..cfc8374a94d 100644 --- a/tests/TypeReconciliation/TypeAlgebraTest.php +++ b/tests/TypeReconciliation/TypeAlgebraTest.php @@ -287,12 +287,12 @@ function foo(array $arr): void { $arr = []; foreach ([0, 1, 2, 3] as $i) { - $a = rand(0, 1) ? 5 : "010"; + $a = (int) (rand(0, 1) ? 5 : "010"); - if (!isset($arr[(int) $a])) { - $arr[(int) $a] = 5; + if (!isset($arr[$a])) { + $arr[$a] = 5; } else { - $arr[(int) $a] += 4; + $arr[$a] += 4; } }', ], From ead29084646d0c1bbb842e981b32ca400bf1f78b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:05:23 +0100 Subject: [PATCH 08/13] Fixup tests --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index fbbb6e445f9..d7b56e5fa48 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,9 +1027,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } From 147129345ea848d0f2559206a106ab13e2c364c2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:23:35 +0100 Subject: [PATCH 09/13] Add failing test --- tests/TypeReconciliation/IssetTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index c15612692e5..19e7dbfcd0e 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -41,6 +41,21 @@ public function providerValidCodeParse(): iterable 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:46:24 +0100 Subject: [PATCH 10/13] Fixup --- src/Psalm/Type/Reconciler.php | 133 ++++++++++++++----------- tests/Loop/ForeachTest.php | 4 +- tests/TypeReconciliation/IssetTest.php | 31 +++++- 3 files changed, 108 insertions(+), 60 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 7863d05c265..61e731862ff 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -46,6 +46,8 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -1108,88 +1110,103 @@ private static function adjustTKeyedArrayType( throw new UnexpectedValueException('Not expecting null array key'); } + $array_key_offsets = []; if ($array_key[0] === '$') { - return; + if (!isset($existing_types[$array_key])) { + return; + } + $t = $existing_types[$array_key]; + foreach ($t->getAtomicTypes() as $lit) { + if ($lit instanceof TLiteralInt || $lit instanceof TLiteralString) { + $array_key_offsets []= $lit->value; + continue; + } + return; + } + } else { + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; } - $array_key_offset = $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; - $base_key = implode($key_parts); - if (isset($existing_types[$base_key]) && $array_key_offset !== false) { - foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { - if ($base_atomic_type instanceof TList) { - $base_atomic_type = $base_atomic_type->getKeyedArray(); - } - if ($base_atomic_type instanceof TKeyedArray + $result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1); + + foreach ($array_key_offsets as $array_key_offset) { + if (isset($existing_types[$base_key]) && $array_key_offset !== false) { + foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { + if ($base_atomic_type instanceof TList) { + $base_atomic_type = $base_atomic_type->getKeyedArray(); + } + if ($base_atomic_type instanceof TKeyedArray || ($base_atomic_type instanceof TArray && !$base_atomic_type->isEmptyArray()) || $base_atomic_type instanceof TClassStringMap - ) { - $new_base_type = $existing_types[$base_key]; + ) { + $new_base_type = $existing_types[$base_key]; - if ($base_atomic_type instanceof TArray) { - $fallback_key_type = $base_atomic_type->type_params[0]; - $fallback_value_type = $base_atomic_type->type_params[1]; + if ($base_atomic_type instanceof TArray) { + $fallback_key_type = $base_atomic_type->type_params[0]; + $fallback_value_type = $base_atomic_type->type_params[1]; - $base_atomic_type = new TKeyedArray( - [ + $base_atomic_type = new TKeyedArray( + [ $array_key_offset => $result_type, - ], - null, - $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], - ); - } elseif ($base_atomic_type instanceof TClassStringMap) { - // do nothing - } else { - $properties = $base_atomic_type->properties; - $properties[$array_key_offset] = $result_type; - if ($base_atomic_type->is_list + ], + null, + $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], + ); + } elseif ($base_atomic_type instanceof TClassStringMap) { + // do nothing + } else { + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = $result_type; + if ($base_atomic_type->is_list && (!is_numeric($array_key_offset) || ($array_key_offset && !isset($properties[$array_key_offset-1]) ) ) - ) { - if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { - $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( - $result_type->isNever(), - ); - for ($x = 0; $x < $array_key_offset; $x++) { - $properties[$x] ??= $fallback; + ) { + if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { + $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( + $result_type->isNever(), + ); + for ($x = 0; $x < $array_key_offset; $x++) { + $properties[$x] ??= $fallback; + } + ksort($properties); + $base_atomic_type = $base_atomic_type->setProperties($properties); + } else { + // This should actually be a paradox + $base_atomic_type = new TKeyedArray( + $properties, + null, + $base_atomic_type->fallback_params, + false, + $base_atomic_type->from_docblock, + ); } - ksort($properties); - $base_atomic_type = $base_atomic_type->setProperties($properties); } else { - // This should actually be a paradox - $base_atomic_type = new TKeyedArray( - $properties, - null, - $base_atomic_type->fallback_params, - false, - $base_atomic_type->from_docblock, - ); + $base_atomic_type = $base_atomic_type->setProperties($properties); } - } else { - $base_atomic_type = $base_atomic_type->setProperties($properties); } - } - $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); - $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; + $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; - if ($key_parts[count($key_parts) - 1] === ']') { - self::adjustTKeyedArrayType( - $key_parts, - $existing_types, - $changed_var_ids, - $new_base_type, - ); - } + if ($key_parts[count($key_parts) - 1] === ']') { + self::adjustTKeyedArrayType( + $key_parts, + $existing_types, + $changed_var_ids, + $new_base_type, + ); + } - $existing_types[$base_key] = $new_base_type; - break; + $existing_types[$base_key] = $new_base_type; + break; + } } } } diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index d7b56e5fa48..fbbb6e445f9 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,7 +1027,9 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - $arr[$i]["a"] ??= 0; + if (!isset($arr[$i]["a"])) { + $arr[$i]["a"] = 0; + } $arr[$i]["a"] += 5; } diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index 19e7dbfcd0e..f9d24846e10 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -54,7 +54,36 @@ function t2(array $arr, int $i): array { $arr[$i] = 1; } return $arr; - }' + }', + ], + 'issetWithArrayAssignment2' => [ + 'code'=> ' [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:48:47 +0100 Subject: [PATCH 11/13] Fixup --- src/Psalm/Type/Reconciler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 61e731862ff..f5e288639cf 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1124,7 +1124,10 @@ private static function adjustTKeyedArrayType( return; } } else { - $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' + ? substr($array_key, 1, -1) + : $array_key + ; } $base_key = implode($key_parts); From 0aeb87c21cd730a5bf7796724653671a5df89b6f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:57:50 +0100 Subject: [PATCH 12/13] Simplify --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index fbbb6e445f9..d7b56e5fa48 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,9 +1027,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } From de53638295356ba5b7a20f2d003be1e927f1e804 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 13:06:36 +0100 Subject: [PATCH 13/13] Fixes --- psalm-baseline.xml | 37 +++++++++++++++++++++-- psalm.xml.dist | 2 +- src/Psalm/Internal/Cli/LanguageServer.php | 8 ++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..71cc2ef6324 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -216,13 +216,15 @@ + + $token_list[$iter] + $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[0] - $token_list[1] @@ -230,6 +232,11 @@ expr->getArgs()[0]]]> + + + + + $identifier_name @@ -350,6 +357,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 07d5e6f93e8..1dc16fbe5bf 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -315,11 +315,9 @@ static function (string $arg) use ($valid_long_options): void { $path_to_config = CliUtils::getPathToConfig($options); - if (isset($options['tcp'])) { - if (!is_string($options['tcp'])) { - fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); - exit(1); - } + if (isset($options['tcp']) && !is_string($options['tcp'])) { + fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); + exit(1); } $config = CliUtils::initializeConfig(