function check_classes(&$classes, $pc_required = []) { global $namespace_map; foreach ($classes as $name => $class) { if (strpos($name, ':') !== false) { list(, $class_name) = explode(':', $name, 2); if ($class['flags'] & \ast\flags\CLASS_INTERFACE) { $class_str = "Interface"; } else { if ($class['flags'] & \ast\flags\CLASS_TRAIT) { $class_str = "Trait"; } else { $class_str = "Class"; } } $orig = $classes[strtolower($class_name)]; if ($orig['flags'] & \ast\flags\CLASS_INTERFACE) { $orig_str = "Interface"; } else { if ($orig['flags'] & \ast\flags\CLASS_TRAIT) { $orig_str = "Trait"; } else { $orig_str = "Class"; } } if ($orig['file'] == 'internal') { Log::err(Log::EREDEF, "{$class_str} {$class_name} defined at {$class['file']}:{$class['lineno']} was previously defined as {$orig_str} {$class_name} internally", $class['file'], $class['lineno']); } else { Log::err(Log::EREDEF, "{$class_str} {$class_name} defined at {$class['file']}:{$class['lineno']} was previously defined as {$orig_str} {$class_name} at {$orig['file']}:{$orig['lineno']}", $class['file'], $class['lineno']); } } else { if ($class['file'] !== 'internal') { $parents = []; $temp = $class; while (!empty($temp['parent'])) { if (empty($classes[strtolower($temp['parent'])])) { Log::err(Log::EUNDEF, "Trying to inherit from unknown class {$temp['parent']}", $class['file'], $class['lineno']); break; } else { if ($classes[strtolower($temp['parent'])]['flags'] & \ast\flags\CLASS_FINAL) { Log::err(Log::ETYPE, "{$class['name']} may not inherit from final class {$classes[strtolower($temp['parent'])]['name']}", $class['file'], $class['lineno']); } } $temp = $classes[strtolower($temp['parent'])]; $parents[] = $temp['name']; if (!empty($temp['interfaces'])) { $parents = array_merge($parents, $temp['interfaces']); } } $types = [$class['name']]; if (!empty($class['interfaces'])) { foreach ($class['interfaces'] as $interface) { if (empty($classes[strtolower($interface)])) { Log::err(Log::EUNDEF, "Trying to implement unknown interface {$interface}", $class['file'], $class['lineno']); } else { if (!($classes[strtolower($interface)]['flags'] & \ast\flags\CLASS_INTERFACE)) { Log::err(Log::ETYPE, "Trying to implement interface {$interface} which is not an interface", $class['file'], $class['lineno']); } } } $types = array_merge($types, $class['interfaces']); } if (!empty($parents)) { $types = array_merge($types, $parents); } // Fill in type from inheritance tree and interfaces $classes[$name]['type'] = merge_type($classes[$name]['type'], implode('|', array_unique($types))); foreach ($pc_required as $c) { if ($class['name'] != $c && in_array($c, $types)) { if (!$class['pc_called']) { Log::err(Log::ETYPE, "{$class['name']} extends {$c} but doesn't call parent::__construct()", $class['file'], $class['lineno']); } } } foreach ($class['methods'] as $k => $method) { if (!($class['methods'][$k]['flags'] & \ast\flags\MODIFIER_STATIC)) { if ($class['flags'] & \ast\flags\CLASS_TRAIT) { // We don't know the type yet for methods in a trait add_var_scope("{$class['name']}::{$method['name']}", 'this', ''); } else { add_var_scope("{$class['name']}::{$method['name']}", 'this', $classes[$name]['type']); } } } } } } }
function arglist_type_check($file, $namespace, $arglist, $func, $current_scope, $current_class) : array { global $internal_arginfo, $scope, $tainted_by; $errs = []; $fn = $func['scope'] ?? $func['name']; foreach ($arglist->children as $k => $arg) { $taint = false; $tainted_by = ''; if (empty($func['params'][$k])) { break; } $param = $func['params'][$k]; $argno = $k + 1; $arg_name = false; if ($param['flags'] & \ast\flags\PARAM_REF) { if (!$arg instanceof \ast\Node || $arg->kind != \ast\AST_VAR && $arg->kind != \ast\AST_DIM && $arg->kind != \ast\AST_PROP) { $errs[] = "Only variables can be passed by reference at arg#{$argno} of {$fn}()"; } else { $arg_name = var_name($arg); } } // For user functions, add the types of the args to the receiving function's scope if ($func['file'] != 'internal') { if (empty($scope[$fn]['vars'][$param['name']])) { $scope[$fn]['vars'][$param['name']] = ['type' => '', 'tainted' => false, 'tainted_by' => '']; } // If it is by-ref link it back to the local variable name if ($param['flags'] & \ast\flags\PARAM_REF) { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint, false); if (!empty($scope[$current_scope]['vars'][$arg_name])) { $scope[$fn]['vars'][$param['name']] =& $scope[$current_scope]['vars'][$arg_name]; } else { $scope[$fn]['vars'][$param['name']]['type'] = $arg_type; } } else { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint); if (!empty($arg_type)) { add_type($fn, $param['name'], $arg_type); } } if ($taint) { $scope[$fn]['vars'][$param['name']]['tainted'] = true; $scope[$fn]['vars'][$param['name']]['tainted_by'] = $tainted_by; } else { $scope[$fn]['vars'][$param['name']]['tainted'] = false; $scope[$fn]['vars'][$param['name']]['tainted_by'] = ''; } } else { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint, !($param['flags'] & \ast\flags\PARAM_REF)); } // For all functions, add the param to the local scope if pass-by-ref // and make it an actual ref for user functions if ($param['flags'] & \ast\flags\PARAM_REF) { if ($func['file'] == 'internal') { if (empty($scope[$current_scope]['vars'][$arg_name])) { add_var_scope($current_scope, $arg_name, $arg_type); } } else { if (empty($scope[$current_scope]['vars'][$arg_name])) { if (!array_key_exists($current_scope, $scope)) { $scope[$current_scope] = []; } if (!array_key_exists('vars', $scope[$current_scope])) { $scope[$current_scope]['vars'] = []; } $scope[$current_scope]['vars'][$arg_name] =& $scope[$fn]['vars'][$param['name']]; } } } // turn callable:{closure n} into just callable if (strpos($arg_type, ':') !== false) { list($arg_type, ) = explode(':', $arg_type, 2); } if (!type_check($arg_type, $param['type'], $namespace)) { if (!empty($param['name'])) { $paramstr = '(' . trim($param['name'], '&=') . ')'; } else { $paramstr = ''; } if (empty($arg_type)) { $arg_type = ''; } if ($func['file'] == 'internal') { if (!($param['flags'] & \ast\flags\PARAM_REF)) { $errs[] = "arg#{$argno}{$paramstr} is {$arg_type} but {$func['name']}() takes {$param['type']}"; } } else { $errs[] = "arg#{$argno}{$paramstr} is {$arg_type} but {$func['name']}() takes {$param['type']} defined at {$func['file']}:{$func['lineno']}"; } } } return $errs; }
function arglist_type_check($file, $namespace, $arglist, $func, $current_scope, $current_class) : array { global $classes, $internal_arginfo, $scope, $tainted_by; $errs = []; $fn = $func['scope'] ?? $func['name']; foreach ($arglist->children as $k => $arg) { $taint = false; $tainted_by = ''; if (empty($func['params'][$k])) { break; } $param = $func['params'][$k]; $argno = $k + 1; $arg_name = false; if ($param['flags'] & \ast\flags\PARAM_REF) { if (!$arg instanceof \ast\Node || $arg->kind != \ast\AST_VAR && $arg->kind != \ast\AST_DIM && $arg->kind != \ast\AST_PROP && $arg->kind != \ast\AST_STATIC_PROP) { $errs[] = "Only variables can be passed by reference at arg#{$argno} of {$fn}()"; } else { $arg_name = var_name($arg); if ($arg->kind == \ast\AST_STATIC_PROP) { if ($arg_name == 'self' || $arg_name == 'static' || $arg_name == 'parent') { Log::err(Log::ESTATIC, "Using {$arg_name}:: when not in object context", $file, $arg->lineno); } } } } // For user functions, add the types of the args to the receiving function's scope if ($func['file'] != 'internal') { if (empty($scope[$fn]['vars'][$param['name']])) { $scope[$fn]['vars'][$param['name']] = ['type' => '', 'tainted' => false, 'tainted_by' => '']; } // If it is by-ref link it back to the local variable name if ($param['flags'] & \ast\flags\PARAM_REF) { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint, false); if ($arg->kind == \ast\AST_STATIC_PROP && $arg->children[0]->kind == \ast\AST_NAME) { $class_name = $arg->children[0]->children[0]; if ($class_name == 'self' || $class_name == 'static' || $class_name == 'parent') { if ($current_class) { if ($class_name == 'static') { $class_name = $current_class['name']; } if ($class_name == 'self') { if ($current_scope != 'global') { list($class_name, ) = explode('::', $current_scope); } else { $class_name = $current_class['name']; } } else { if ($class_name == 'parent') { $class_name = $current_class['parent']; } } $static_call_ok = true; } else { $class_name = ''; } } else { $class_name = qualified_name($file, $arg->children[0], $namespace); } if ($class_name) { if (!$arg->children[1] instanceof \ast\Node) { if (empty($classes[strtolower($class_name)]['properties'][$arg->children[1]])) { Log::err(Log::ESTATIC, "Access to undeclared static property: {$class_name}::\${$arg->children[1]}", $file, $arg->lineno); } else { $scope[$fn]['vars'][$param['name']] =& $classes[strtolower($class_name)]['properties'][$arg->children[1]]; } } } } else { if (!empty($scope[$current_scope]['vars'][$arg_name])) { $scope[$fn]['vars'][$param['name']] =& $scope[$current_scope]['vars'][$arg_name]; } else { $scope[$fn]['vars'][$param['name']]['type'] = $arg_type; } } } else { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint); if (!empty($arg_type)) { add_type($fn, $param['name'], $arg_type); } } if ($taint) { $scope[$fn]['vars'][$param['name']]['tainted'] = true; $scope[$fn]['vars'][$param['name']]['tainted_by'] = $tainted_by; } else { $scope[$fn]['vars'][$param['name']]['tainted'] = false; $scope[$fn]['vars'][$param['name']]['tainted_by'] = ''; } } else { $arg_type = node_type($file, $namespace, $arg, $current_scope, $current_class, $taint, !($param['flags'] & \ast\flags\PARAM_REF)); } // For all functions, add the param to the local scope if pass-by-ref // and make it an actual ref for user functions if ($param['flags'] & \ast\flags\PARAM_REF) { if ($func['file'] == 'internal') { if (empty($scope[$current_scope]['vars'][$arg_name])) { add_var_scope($current_scope, $arg_name, $arg_type); } } else { if (empty($scope[$current_scope]['vars'][$arg_name])) { if (!array_key_exists($current_scope, $scope)) { $scope[$current_scope] = []; } if (!array_key_exists('vars', $scope[$current_scope])) { $scope[$current_scope]['vars'] = []; } $scope[$current_scope]['vars'][$arg_name] =& $scope[$fn]['vars'][$param['name']]; } } } // turn callable:{closure n} into just callable if (strpos($arg_type, ':') !== false) { list($arg_type, ) = explode(':', $arg_type, 2); } // if we have a single non-native type, expand it if (!empty($arg_type) && !is_native_type($arg_type)) { if (!empty($classes[strtolower($arg_type)]['type'])) { $arg_type = $classes[strtolower($arg_type)]['type']; } } if (!type_check(all_types($arg_type), all_types($param['type']), $namespace)) { if (!empty($param['name'])) { $paramstr = '(' . trim($param['name'], '&=') . ')'; } else { $paramstr = ''; } if (empty($arg_type)) { $arg_type = ''; } if ($func['file'] == 'internal') { if (!($param['flags'] & \ast\flags\PARAM_REF)) { $errs[] = "arg#{$argno}{$paramstr} is {$arg_type} but {$func['name']}() takes {$param['type']}"; } } else { $errs[] = "arg#{$argno}{$paramstr} is {$arg_type} but {$func['name']}() takes {$param['type']} defined at {$func['file']}:{$func['lineno']}"; } } } return $errs; }
function pass1($file, $namespace, $conditional, $ast, $current_scope, $current_class = null, $current_function = null) { global $classes, $functions, $namespace_map, $summary; $done = false; if ($ast instanceof \ast\Node) { switch ($ast->kind) { case \ast\AST_NAMESPACE: $namespace = (string) $ast->children[0] . '\\'; break; case \ast\AST_IF: $conditional = true; $summary['conditionals']++; break; case \ast\AST_USE: foreach ($ast->children as $elem) { $target = $elem->children[0]; if (empty($elem->children[1])) { if (($pos = strrpos($target, '\\')) !== false) { $alias = substr($target, $pos + 1); } else { $alias = $target; } } else { $alias = $elem->children[1]; } $namespace_map[$ast->flags][$file][strtolower($alias)] = $target; } break; case \ast\AST_CLASS: if (!empty($classes[strtolower($namespace . $ast->name)])) { for ($i = 1;; $i++) { if (empty($classes[$i . ":" . strtolower($namespace . $ast->name)])) { break; } } $current_class = $i . ":" . $namespace . $ast->name; } else { $current_class = $namespace . $ast->name; } if (!empty($ast->children[0])) { $parent = $ast->children[0]->children[0]; if ($ast->children[0]->flags & \ast\flags\NAME_NOT_FQ) { if (($pos = strpos($parent, '\\')) !== false) { // extends A\B // check if we have a namespace alias for A if (!empty($namespace_map[T_CLASS][$file][strtolower(substr($parent, 0, $pos))])) { $parent = $namespace_map[T_CLASS][$file][strtolower(substr($parent, 0, $pos))] . substr($parent, $pos); goto done; } } $parent = $namespace_map[T_CLASS][$file][strtolower($parent)] ?? $namespace . $parent; done: } } else { $parent = null; } $classes[strtolower($current_class)] = ['file' => $file, 'namespace' => $namespace, 'conditional' => $conditional, 'flags' => $ast->flags, 'lineno' => $ast->lineno, 'endLineno' => $ast->endLineno, 'name' => $namespace . $ast->name, 'docComment' => $ast->docComment, 'parent' => $parent, 'type' => '', 'properties' => [], 'constants' => [], 'traits' => [], 'interfaces' => [], 'methods' => []]; $classes[strtolower($current_class)]['interfaces'] = array_merge($classes[strtolower($current_class)]['interfaces'], node_namelist($file, $ast->children[1], $namespace)); $summary['classes']++; break; case \ast\AST_USE_TRAIT: $classes[strtolower($current_class)]['traits'] = array_merge($classes[strtolower($current_class)]['traits'], node_namelist($file, $ast->children[0], $namespace)); $summary['traits']++; break; case \ast\AST_METHOD: if (!empty($classes[strtolower($current_class)]['methods'][strtolower($ast->name)])) { for ($i = 1;; $i++) { if (empty($classes[strtolower($current_class)]['methods'][$i . ':' . strtolower($ast->name)])) { break; } } $method = $i . ':' . $ast->name; } else { $method = $ast->name; } $classes[strtolower($current_class)]['methods'][strtolower($method)] = node_func($file, $conditional, $ast, "{$current_class}::{$method}", $current_class, $namespace); if (!($classes[strtolower($current_class)]['methods'][strtolower($method)]['flags'] & \ast\flags\MODIFIER_STATIC)) { add_var_scope("{$current_class}::{$method}", 'this', $current_class); } $summary['methods']++; $current_function = $method; $current_scope = "{$current_class}::{$method}"; break; case \ast\AST_PROP_DECL: if (empty($current_class)) { Log::err(Log::EFATAL, "Invalid property declaration", $file, $ast->lineno); } foreach ($ast->children as $node) { $classes[strtolower($current_class)]['properties'][$node->children[0]] = ['flags' => $ast->flags, 'name' => $node->children[0], 'lineno' => $node->lineno, 'value' => node_type($file, $namespace, $node->children[1], $current_scope, $current_class)]; } $done = true; break; case \ast\AST_CLASS_CONST_DECL: if (empty($current_class)) { Log::err(Log::EFATAL, "Invalid constant declaration", $file, $ast->lineno); } foreach ($ast->children as $node) { $classes[strtolower($current_class)]['constants'][$node->children[0]] = ['name' => $node->children[0], 'lineno' => $node->lineno, 'value' => $node->children[1]]; } $done = true; break; case \ast\AST_FUNC_DECL: if (!empty($functions[strtolower($namespace . $ast->name)])) { for ($i = 1;; $i++) { if (empty($functions[$i . ":" . strtolower($namespace . $ast->name)])) { break; } } $function = $i . ':' . $namespace . $ast->name; } else { $function = $namespace . $ast->name; } $functions[strtolower($function)] = node_func($file, $conditional, $ast, $function, $current_class, $namespace); $summary['functions']++; $current_function = $function; $current_scope = $function; // Not $done=true here since nested function declarations are allowed break; case \ast\AST_CLOSURE: $summary['closures']++; $current_scope = "{closure}"; break; case \ast\AST_CALL: // Looks odd to check for AST_CALL in pass1, but we need to see if a function calls func_get_arg/func_get_args/func_num_args $found = false; $call = $ast->children[0]; if ($call->kind == \ast\AST_NAME) { $func_name = strtolower($call->children[0]); if ($func_name == 'func_get_args' || $func_name == 'func_get_arg' || $func_name == 'func_num_args') { if (!empty($current_class)) { $classes[strtolower($current_class)]['methods'][strtolower($current_function)]['optional'] = 999999; } else { $functions[strtolower($current_function)]['optional'] = 999999; } } } break; } if (!$done) { foreach ($ast->children as $child) { $namespace = pass1($file, $namespace, $conditional, $child, $current_scope, $current_class, $current_function); } } } return $namespace; }
function pass1($file, $namespace, $conditional, $ast, $current_scope, $current_class = null, $current_function = null) { global $classes, $functions, $namespace_map, $summary, $bc_checks; $done = false; $lc = strtolower((string) $current_class); if ($ast instanceof \ast\Node) { switch ($ast->kind) { case \ast\AST_NAMESPACE: $namespace = (string) $ast->children[0] . '\\'; break; case \ast\AST_IF: $conditional = true; $summary['conditionals']++; break; case \ast\AST_DIM: if ($bc_checks) { if ($ast->children[0] instanceof \ast\Node && $ast->children[0]->children[0] instanceof \ast\Node) { // check for $$var[] if ($ast->children[0]->kind == \ast\AST_VAR && $ast->children[0]->children[0]->kind == \ast\AST_VAR) { $temp = $ast->children[0]->children[0]; $depth = 1; while ($temp instanceof \ast\Node) { $temp = $temp->children[0]; $depth++; } $dollars = str_repeat('$', $depth); $ftemp = new \SplFileObject($file); $ftemp->seek($ast->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '{') === false || strpos($line, '}') === false) { Log::err(Log::ECOMPAT, "{$dollars}{$temp}[] expression may not be PHP 7 compatible", $file, $ast->lineno); } } else { if ($ast->children[0]->children[1] instanceof \ast\Node && $ast->children[0]->kind == \ast\AST_PROP && $ast->children[0]->children[0]->kind == \ast\AST_VAR && $ast->children[0]->children[1]->kind == \ast\AST_VAR) { $ftemp = new \SplFileObject($file); $ftemp->seek($ast->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '{') === false || strpos($line, '}') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $file, $ast->lineno); } } } } } break; case \ast\AST_USE: foreach ($ast->children as $elem) { $target = $elem->children[0]; if (empty($elem->children[1])) { if (($pos = strrpos($target, '\\')) !== false) { $alias = substr($target, $pos + 1); } else { $alias = $target; } } else { $alias = $elem->children[1]; } $namespace_map[$ast->flags][$file][strtolower($alias)] = $target; } break; case \ast\AST_CLASS: if (!empty($classes[strtolower($namespace . $ast->name)])) { for ($i = 1;; $i++) { if (empty($classes[$i . ":" . strtolower($namespace . $ast->name)])) { break; } } $current_class = $i . ":" . $namespace . $ast->name; } else { $current_class = $namespace . $ast->name; } $lc = strtolower($current_class); if (!empty($ast->children[0])) { $parent = $ast->children[0]->children[0]; if ($ast->children[0]->flags & \ast\flags\NAME_NOT_FQ) { if (($pos = strpos($parent, '\\')) !== false) { // extends A\B // check if we have a namespace alias for A if (!empty($namespace_map[T_CLASS][$file][strtolower(substr($parent, 0, $pos))])) { $parent = $namespace_map[T_CLASS][$file][strtolower(substr($parent, 0, $pos))] . substr($parent, $pos); goto done; } } $parent = $namespace_map[T_CLASS][$file][strtolower($parent)] ?? $namespace . $parent; done: } } else { $parent = null; } $classes[$lc] = ['file' => $file, 'namespace' => $namespace, 'conditional' => $conditional, 'flags' => $ast->flags, 'lineno' => $ast->lineno, 'endLineno' => $ast->endLineno, 'name' => $namespace . $ast->name, 'docComment' => $ast->docComment, 'parent' => $parent, 'type' => '', 'properties' => [], 'constants' => [], 'traits' => [], 'interfaces' => [], 'methods' => []]; $classes[$lc]['interfaces'] = array_merge($classes[$lc]['interfaces'], node_namelist($file, $ast->children[1], $namespace)); $summary['classes']++; break; case \ast\AST_USE_TRAIT: $classes[$lc]['traits'] = array_merge($classes[$lc]['traits'], node_namelist($file, $ast->children[0], $namespace)); $summary['traits']++; break; case \ast\AST_METHOD: if (!empty($classes[$lc]['methods'][strtolower($ast->name)])) { for ($i = 1;; $i++) { if (empty($classes[$lc]['methods'][$i . ':' . strtolower($ast->name)])) { break; } } $method = $i . ':' . $ast->name; } else { $method = $ast->name; } $classes[$lc]['methods'][strtolower($method)] = node_func($file, $conditional, $ast, "{$current_class}::{$method}", $current_class, $namespace); if (!($classes[$lc]['methods'][strtolower($method)]['flags'] & \ast\flags\MODIFIER_STATIC)) { add_var_scope("{$current_class}::{$method}", 'this', $current_class); } $summary['methods']++; $current_function = $method; $current_scope = "{$current_class}::{$method}"; break; case \ast\AST_PROP_DECL: if (empty($current_class)) { Log::err(Log::EFATAL, "Invalid property declaration", $file, $ast->lineno); } $dc = null; if (!empty($ast->docComment)) { $dc = parse_doc_comment($ast->docComment); } foreach ($ast->children as $i => $node) { $classes[$lc]['properties'][$node->children[0]] = ['flags' => $ast->flags, 'name' => $node->children[0], 'lineno' => $node->lineno]; $type = node_type($file, $namespace, $node->children[1], $current_scope, empty($classes[$lc]) ? null : $classes[$lc]); if (!empty($dc['vars'][$i]['type'])) { if ($type !== 'NULL' && !type_check($type, $dc['vars'][$i]['type'])) { Log::err(Log::ETYPE, "property is declared to be {$dc['vars'][$i]['type']} but was assigned {$type}", $file, $node->lineno); } $classes[$lc]['properties'][$node->children[0]]['dtype'] = $dc['vars'][$i]['type']; $classes[$lc]['properties'][$node->children[0]]['type'] = $dc['vars'][$i]['type']; if (!empty($type) && $type != $classes[$lc]['properties'][$node->children[0]]['type']) { $classes[$lc]['properties'][$node->children[0]]['type'] .= '|' . strtolower($type); } } else { $classes[$lc]['properties'][$node->children[0]]['dtype'] = ''; $classes[$lc]['properties'][$node->children[0]]['type'] = $type; } } $done = true; break; case \ast\AST_CLASS_CONST_DECL: if (empty($current_class)) { Log::err(Log::EFATAL, "Invalid constant declaration", $file, $ast->lineno); } foreach ($ast->children as $node) { $classes[$lc]['constants'][$node->children[0]] = ['name' => $node->children[0], 'lineno' => $node->lineno, 'type' => node_type($file, $namespace, $node->children[1], $current_scope, empty($classes[$lc]) ? null : $classes[$lc])]; } $done = true; break; case \ast\AST_FUNC_DECL: if (!empty($functions[strtolower($namespace . $ast->name)])) { for ($i = 1;; $i++) { if (empty($functions[$i . ":" . strtolower($namespace . $ast->name)])) { break; } } $function = $i . ':' . $namespace . $ast->name; } else { $function = $namespace . $ast->name; } $functions[strtolower($function)] = node_func($file, $conditional, $ast, $function, $current_class, $namespace); $summary['functions']++; $current_function = $function; $current_scope = $function; // Not $done=true here since nested function declarations are allowed break; case \ast\AST_CLOSURE: $summary['closures']++; $current_scope = "{closure}"; break; case \ast\AST_CALL: // Looks odd to check for AST_CALL in pass1, but we need to see if a function calls func_get_arg/func_get_args/func_num_args $found = false; $call = $ast->children[0]; if ($call->kind == \ast\AST_NAME) { $func_name = strtolower($call->children[0]); if ($func_name == 'func_get_args' || $func_name == 'func_get_arg' || $func_name == 'func_num_args') { if (!empty($current_class)) { $classes[$lc]['methods'][strtolower($current_function)]['optional'] = 999999; } else { $functions[strtolower($current_function)]['optional'] = 999999; } } } if ($bc_checks) { if ($ast->children[0] instanceof \ast\Node && $ast->children[0]->children[0] instanceof \ast\Node && $ast->children[0]->children[0]->children[0] instanceof \ast\Node) { // $foo->$bar['baz']() if ($ast->children[0]->kind == \ast\AST_DIM && $ast->children[0]->children[0]->kind == \ast\AST_PROP && $ast->children[0]->children[0]->children[0]->kind == \ast\AST_VAR) { $ftemp = new \SplFileObject($file); $ftemp->seek($ast->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '{') === false || strpos($line, '}') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $file, $ast->lineno); } } // Foo::$bar['baz']() if ($ast->children[0]->kind == \ast\AST_DIM && $ast->children[0]->children[0]->kind == \ast\AST_STATIC_PROP && $ast->children[0]->children[0]->children[0]->kind == \ast\AST_NAME) { $ftemp = new \SplFileObject($file); $ftemp->seek($ast->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '{') === false || strpos($line, '}') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $file, $ast->lineno); } } } } break; } if (!$done) { foreach ($ast->children as $child) { $namespace = pass1($file, $namespace, $conditional, $child, $current_scope, $current_class, $current_function); } } } return $namespace; }
function node_func($file, $conditional, $node, $current_scope, $current_class, $namespace = '') { global $scope, $classes; if ($node instanceof \ast\Node) { $req = $opt = 0; $dc = ['return' => '', 'params' => []]; if (!empty($node->docComment)) { $dc = parse_doc_comment($node->docComment); } $result = ['file' => $file, 'namespace' => $namespace, 'scope' => $current_scope, 'conditional' => $conditional, 'flags' => $node->flags, 'lineno' => $node->lineno, 'endLineno' => $node->endLineno, 'name' => strpos($current_scope, '::') === false ? $namespace . $node->name : $node->name, 'docComment' => $node->docComment, 'params' => node_paramlist($file, $node->children[0], $req, $opt, $dc, $namespace), 'required' => $req, 'optional' => $opt, 'ret' => '', 'oret' => '', 'ast' => $node->children[2]]; if (!empty($dc['deprecated'])) { $result['deprecated'] = true; } if ($node->children[3] !== null) { $result['oret'] = ast_node_type($file, $node->children[3], $namespace); // Original return type $result['ret'] = ast_node_type($file, $node->children[3], $namespace); // This one changes as we walk the tree } else { // Check if the docComment has a return value specified if (!empty($dc['return'])) { // We can't actually figure out 'static' at this point, but fill it in regardless. It will be partially correct if ($dc['return'] == 'static' || $dc['return'] == 'self' || $dc['return'] == '$this') { if (strpos($current_scope, '::') !== false) { list($dc['return'], ) = explode('::', $current_scope); } } $result['oret'] = $dc['return']; $result['ret'] = $dc['return']; } } // Add params to local scope for user functions if ($file != 'internal') { $i = 1; foreach ($result['params'] as $k => $v) { if (empty($v['type'])) { // If there is no type specified in PHP, check for a docComment // We assume order in the docComment matches the parameter order in the code if (!empty($dc['params'][$k]['type'])) { $scope[$current_scope]['vars'][$v['name']] = ['type' => $dc['params'][$k]['type'], 'tainted' => false, 'tainted_by' => '', 'param' => $i]; } else { $scope[$current_scope]['vars'][$v['name']] = ['type' => '', 'tainted' => false, 'tainted_by' => '', 'param' => $i]; } } else { $scope[$current_scope]['vars'][$v['name']] = ['type' => $v['type'], 'tainted' => false, 'tainted_by' => '', 'param' => $i]; } if (array_key_exists('def', $v)) { $type = node_type($file, $namespace, $v['def'], $current_scope, empty($current_class) ? null : $classes[strtolower($current_class)]); if ($scope[$current_scope]['vars'][$v['name']]['type'] !== '') { // Does the default value match the declared type? if ($type !== 'null' && !type_check($type, $scope[$current_scope]['vars'][$v['name']]['type'])) { Log::err(Log::ETYPE, "Default value for {$scope[$current_scope]['vars'][$v['name']]['type']} \${$v['name']} can't be {$type}", $file, $node->lineno); } } add_type($current_scope, $v['name'], strtolower($type)); // If we have no other type info about a parameter, just because it has a default value of null // doesn't mean that is its type. Any type can default to null if ($type === 'null' && !empty($result['params'][$k]['type'])) { $result['params'][$k]['type'] = merge_type($result['params'][$k]['type'], strtolower($type)); } } $i++; } if (!empty($dc['vars'])) { foreach ($dc['vars'] as $var) { add_var_scope($current_scope, $var['name'], $var['type']); } } } return $result; } assert(false, "{$node} was not an \\ast\\Node"); }