function arg_check(string $file, $namespace, $ast, string $func_name, $func, string $current_scope, $current_class, string $class_name = '') { global $internal_arginfo, $functions, $scope; $ok = false; $varargs = false; $taint = false; // Are we calling it with the right number of args? if ($ast->kind == \ast\AST_CALL || $ast->kind == \ast\AST_NEW) { $arglist = $ast->children[1]; } else { $arglist = $ast->children[2]; } $argcount = count($arglist->children); // Special common cases where we want slightly better multi-signature error messages if ($func['file'] == 'internal') { switch ($func['name']) { case 'join': case 'implode': // (string glue, array pieces), (array pieces, string glue) or (array pieces) if ($argcount == 1) { // If we have just one arg it must be an array if (($arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class)) != 'array') { Log::err(Log::ETYPE, "arg#1(pieces) is {$arg_type} but {$func['name']}() takes array when passed only 1 arg", $file, $ast->lineno); } return; } else { if ($argcount == 2) { $arg1_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class); $arg2_type = node_type($file, $namespace, $arglist->children[1], $current_scope, $current_class); if ($arg1_type == 'array') { if (!type_check($arg2_type, 'string')) { Log::err(Log::ETYPE, "arg#2(glue) is {$arg2_type} but {$func['name']}() takes string when arg#1 is array", $file, $ast->lineno); } } else { if ($arg1_type == 'string') { if (!type_check($arg2_type, 'array')) { Log::err(Log::ETYPE, "arg#2(pieces) is {$arg2_type} but {$func['name']}() takes array when arg#1 is string", $file, $ast->lineno); } } } return; } } // Any other arg counts we will let the regular checks handle break; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token if (($arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class)) != 'string') { Log::err(Log::ETYPE, "arg#1(token) is {$arg_type} but {$func['name']}() takes string when passed only one arg", $file, $ast->lineno); return; } } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (($arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class)) != 'array') { Log::err(Log::ETYPE, "arg#1(values) is {$arg_type} but {$func['name']}() takes array when passed only one arg", $file, $ast->lineno); return; } } $varargs = true; // The arginfo check will handle the other case break; default: if (internal_varargs_check($func['name'])) { $varargs = true; } break; } } else { foreach ($func['params'] as $param) { if ($param['flags'] & \ast\flags\PARAM_VARIADIC) { $varargs = true; } } } $fn = $func['scope'] ?? $func['name']; if ($argcount < $func['required']) { $err = true; $alt = 1; // Check if there is an alternate signature that is ok while (!empty($functions["{$func['name']} {$alt}"])) { if ($argcount < $functions["{$func['name']} {$alt}"]['required']) { $alt++; } else { $err = false; break; } } if ($err) { if ($func['file'] == 'internal') { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() that requires {$func['required']} arg(s)", $file, $ast->lineno); } else { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() that requires {$func['required']} arg(s) defined at {$func['file']}:{$func['lineno']}", $file, $ast->lineno); } } } if (!$varargs && $argcount > $func['required'] + $func['optional']) { $err = true; $alt = 1; // Check if there is an alternate signature that is ok while (!empty($functions["{$func['name']} {$alt}"])) { if ($argcount > $functions["{$func['name']} {$alt}"]['required'] + $functions["{$func['name']} {$alt}"]['optional']) { $alt++; } else { $err = false; break; } } if ($err) { $max = $func['required'] + $func['optional']; if ($func['file'] == 'internal') { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() that only takes {$max} arg(s)", $file, $ast->lineno); } else { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() that only takes {$max} arg(s) defined at {$func['file']}:{$func['lineno']}", $file, $ast->lineno); } } } // Are the types right? // Check if we have any alternate arginfo signatures // Checking the alternates before the main to make the final error messages, if any, refer to the main signature $errs = []; $alt = 1; while (!empty($functions["{$func['name']} {$alt}"])) { $errs = arglist_type_check($file, $namespace, $arglist, $functions["{$func['name']} {$alt}"], $current_scope, $current_class); $alt++; if (empty($errs)) { break; } } if ($alt == 1 || $alt > 1 && !empty($errs)) { $errs = arglist_type_check($file, $namespace, $arglist, $func, $current_scope, $current_class); } foreach ($errs as $err) { Log::err(Log::ETYPE, $err, $file, $ast->lineno); } }
function arg_check(string $file, $namespace, $ast, string $func_name, $func, string $current_scope, $current_class, string $class_name = '') { global $internal_arginfo, $classes, $functions, $scope; $ok = false; $varargs = false; $unpack = false; $taint = false; if ($ast->kind == \ast\AST_CALL || $ast->kind == \ast\AST_NEW) { $arglist = $ast->children[1]; } else { $arglist = $ast->children[2]; } $argcount = count($arglist->children); // Special common cases where we want slightly better multi-signature error messages if ($func['file'] == 'internal') { switch ($func['name']) { case 'join': case 'implode': // (string glue, array pieces), (array pieces, string glue) or (array pieces) if ($argcount == 1) { // If we have just one arg it must be an array $arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class); if (!type_check($arg_type, 'array')) { Log::err(Log::EPARAM, "arg#1(pieces) is {$arg_type} but {$func['name']}() takes array when passed only 1 arg", $file, $ast->lineno); } return; } else { if ($argcount == 2) { $arg1_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class); $arg2_type = node_type($file, $namespace, $arglist->children[1], $current_scope, $current_class); if ($arg1_type == 'array') { if (!type_check($arg2_type, 'string')) { Log::err(Log::EPARAM, "arg#2(glue) is {$arg2_type} but {$func['name']}() takes string when arg#1 is array", $file, $ast->lineno); } } else { if ($arg1_type == 'string') { if (!type_check($arg2_type, 'array')) { Log::err(Log::EPARAM, "arg#2(pieces) is {$arg2_type} but {$func['name']}() takes array when arg#1 is string", $file, $ast->lineno); } } } return; } } // Any other arg counts we will let the regular checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which requires {$func['required']} arg(s)", $file, $ast->lineno); return; } // The last argument must be a callable and there can be a variable number of arrays before it $arg_type = node_type($file, $namespace, $arglist->children[$argcount - 1], $current_scope, $current_class); if (!type_check($arg_type, 'callable')) { Log::err(Log::EPARAM, "The last argument to {$func['name']} must be a callable", $file, $ast->lineno); } for ($i = 0; $i < $argcount - 1; $i++) { $arg_type = node_type($file, $namespace, $arglist->children[$i], $current_scope, $current_class); if (!type_check($arg_type, 'array')) { Log::err(Log::EPARAM, "arg#" . ($i + 1) . " is {$arg_type} but {$func['name']}() takes array", $file, $ast->lineno); } } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which requires {$func['required']} arg(s)", $file, $ast->lineno); return; } // The last 2 arguments must be a callable and there can be a variable number of arrays before it $arg_type = node_type($file, $namespace, $arglist->children[$argcount - 1], $current_scope, $current_class); if (!type_check($arg_type, 'callable')) { Log::err(Log::EPARAM, "The last argument to {$func['name']} must be a callable", $file, $ast->lineno); } $arg_type = node_type($file, $namespace, $arglist->children[$argcount - 2], $current_scope, $current_class); if (!type_check($arg_type, 'callable')) { Log::err(Log::EPARAM, "The second last argument to {$func['name']} must be a callable", $file, $ast->lineno); } for ($i = 0; $i < $argcount - 2; $i++) { $arg_type = node_type($file, $namespace, $arglist->children[$i], $current_scope, $current_class); if (!type_check($arg_type, 'array')) { Log::err(Log::EPARAM, "arg#" . ($i + 1) . " is {$arg_type} but {$func['name']}() takes array", $file, $ast->lineno); } } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token $arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class); if (!type_check($arg_type, 'string')) { Log::err(Log::EPARAM, "arg#1(token) is {$arg_type} but {$func['name']}() takes string when passed only one arg", $file, $ast->lineno); return; } } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array $arg_type = node_type($file, $namespace, $arglist->children[0], $current_scope, $current_class); if (!type_check($arg_type, 'array')) { Log::err(Log::EPARAM, "arg#1(values) is {$arg_type} but {$func['name']}() takes array when passed only one arg", $file, $ast->lineno); return; } } $varargs = true; // The arginfo check will handle the other case break; default: if (internal_varargs_check($func['name'])) { $varargs = true; } break; } } else { if (!empty($func['deprecated'])) { Log::err(Log::EDEP, "Call to deprecated function {$func['name']}() defined at {$func['file']}:{$func['lineno']}", $file, $ast->lineno); } } if (array_key_exists('params', $func)) { foreach ($func['params'] as $param) { if ($param['flags'] & \ast\flags\PARAM_VARIADIC) { $varargs = true; } } } foreach ($arglist->children as $arg) { if ($arg instanceof \ast\Node && $arg->kind == \ast\AST_UNPACK) { $unpack = true; } } $fn = $func['scope'] ?? $func['name']; if (!$unpack && $argcount < $func['required']) { $err = true; $alt = 1; // Check if there is an alternate signature that is ok while (!empty($functions["{$func['name']} {$alt}"])) { if ($argcount < $functions["{$func['name']} {$alt}"]['required']) { $alt++; } else { $err = false; break; } } if ($err) { if ($func['file'] == 'internal') { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which requires {$func['required']} arg(s)", $file, $ast->lineno); } else { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which requires {$func['required']} arg(s) defined at {$func['file']}:{$func['lineno']}", $file, $ast->lineno); } } } if (!$varargs && $argcount > $func['required'] + $func['optional']) { $err = true; $alt = 1; // Check if there is an alternate signature that is ok while (!empty($functions["{$func['name']} {$alt}"])) { if ($argcount > $functions["{$func['name']} {$alt}"]['required'] + $functions["{$func['name']} {$alt}"]['optional']) { $alt++; } else { $err = false; break; } } // For method calls, we have no way of knowing the actual signature. // We may only have the base signature from an interface, for example // and the actual called method could have extra optional args if ($err && $ast->kind != \ast\AST_METHOD_CALL) { $max = $func['required'] + $func['optional']; if ($func['file'] == 'internal') { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which only takes {$max} arg(s)", $file, $ast->lineno); } else { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$func['name']}() which only takes {$max} arg(s) defined at {$func['file']}:{$func['lineno']}", $file, $ast->lineno); } } } // Are the types right? // Check if we have any alternate arginfo signatures // Checking the alternates before the main to make the final error messages, if any, refer to the main signature $errs = []; $alt = 1; if ($class_name) { $lc = strtolower($class_name); $lfn = strtolower($func['name']); $func['name'] = $class_name . '::' . $func['name']; while (!empty($classes[$lc]['methods']["{$lfn} {$alt}"])) { $errs = arglist_type_check($file, $namespace, $arglist, $classes[$lc]['methods']["{$lfn} {$alt}"], $current_scope, $current_class); $alt++; if (empty($errs)) { break; } } } else { while (!empty($functions["{$func['name']} {$alt}"])) { $errs = arglist_type_check($file, $namespace, $arglist, $functions["{$func['name']} {$alt}"], $current_scope, $current_class); $alt++; if (empty($errs)) { break; } } } if ($alt == 1 || $alt > 1 && !empty($errs)) { $errs = arglist_type_check($file, $namespace, $arglist, $func, $current_scope, $current_class); } foreach ($errs as $err) { Log::err(Log::ETYPE, $err, $file, $ast->lineno); } }